mirror of
https://github.com/google/nomulus
synced 2026-02-02 19:12:27 +00:00
Compare commits
9 Commits
nomulus-20
...
nomulus-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90eb078e3f | ||
|
|
2a94bdc257 | ||
|
|
50fa49e0c0 | ||
|
|
a581259edb | ||
|
|
fcdac3e86e | ||
|
|
b652f81193 | ||
|
|
d98d65eee5 | ||
|
|
28e72bd0d0 | ||
|
|
0777be3d6c |
@@ -28,13 +28,20 @@ import static google.registry.request.RequestParameters.extractRequiredDatetimeP
|
||||
import static google.registry.request.RequestParameters.extractRequiredParameter;
|
||||
import static google.registry.request.RequestParameters.extractSetOfDatetimeParameters;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.util.concurrent.RateLimiter;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import google.registry.request.HttpException.BadRequestException;
|
||||
import google.registry.request.OptionalJsonPayload;
|
||||
import google.registry.request.Parameter;
|
||||
import jakarta.inject.Named;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
@@ -44,6 +51,8 @@ public class BatchModule {
|
||||
|
||||
public static final String PARAM_FAST = "fast";
|
||||
|
||||
static final int DEFAULT_MAX_QPS = 10;
|
||||
|
||||
@Provides
|
||||
@Parameter("url")
|
||||
static String provideUrl(HttpServletRequest req) {
|
||||
@@ -140,8 +149,6 @@ public class BatchModule {
|
||||
return extractBooleanParameter(req, PARAM_FAST);
|
||||
}
|
||||
|
||||
private static final int DEFAULT_MAX_QPS = 10;
|
||||
|
||||
@Provides
|
||||
@Parameter("maxQps")
|
||||
static int provideMaxQps(HttpServletRequest req) {
|
||||
@@ -149,8 +156,42 @@ public class BatchModule {
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("removeAllDomainContacts")
|
||||
static RateLimiter provideRemoveAllDomainContactsRateLimiter(@Parameter("maxQps") int maxQps) {
|
||||
@Named("standardRateLimiter")
|
||||
static RateLimiter provideStandardRateLimiter(@Parameter("maxQps") int maxQps) {
|
||||
return RateLimiter.create(maxQps);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Parameter("gainingRegistrarId")
|
||||
static String provideGainingRegistrarId(HttpServletRequest req) {
|
||||
return extractRequiredParameter(req, "gainingRegistrarId");
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Parameter("losingRegistrarId")
|
||||
static String provideLosingRegistrarId(HttpServletRequest req) {
|
||||
return extractRequiredParameter(req, "losingRegistrarId");
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Parameter("bulkTransferDomainNames")
|
||||
static ImmutableList<String> provideBulkTransferDomainNames(
|
||||
Gson gson, @OptionalJsonPayload Optional<JsonElement> optionalJsonElement) {
|
||||
return optionalJsonElement
|
||||
.map(je -> ImmutableList.copyOf(gson.fromJson(je, new TypeToken<List<String>>() {})))
|
||||
.orElseThrow(
|
||||
() -> new BadRequestException("Missing POST body of bulk transfer domain names"));
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Parameter("requestedByRegistrar")
|
||||
static boolean provideRequestedByRegistrar(HttpServletRequest req) {
|
||||
return extractBooleanParameter(req, "requestedByRegistrar");
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Parameter("reason")
|
||||
static String provideReason(HttpServletRequest req) {
|
||||
return extractRequiredParameter(req, "reason");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.batch;
|
||||
|
||||
import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8;
|
||||
import static google.registry.flows.FlowUtils.marshalWithLenientRetry;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_NO_CONTENT;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
|
||||
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.util.concurrent.RateLimiter;
|
||||
import google.registry.flows.EppController;
|
||||
import google.registry.flows.EppRequestSource;
|
||||
import google.registry.flows.PasswordOnlyTransportCredentials;
|
||||
import google.registry.flows.StatelessRequestSessionMetadata;
|
||||
import google.registry.model.ForeignKeyUtils;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.eppcommon.ProtocolDefinition;
|
||||
import google.registry.model.eppcommon.StatusValue;
|
||||
import google.registry.model.eppoutput.EppOutput;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.Parameter;
|
||||
import google.registry.request.Response;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.request.lock.LockHandler;
|
||||
import google.registry.util.DateTimeUtils;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.logging.Level;
|
||||
import org.joda.time.Duration;
|
||||
|
||||
/**
|
||||
* An action that transfers a set of domains from one registrar to another.
|
||||
*
|
||||
* <p>This should be used as part of the BTAPPA (Bulk Transfer After a Partial Portfolio
|
||||
* Acquisition) process in order to transfer a (possibly large) list of domains from one registrar
|
||||
* to another, though it may be used in other situations as well.
|
||||
*
|
||||
* <p>The body of the HTTP post request should be a JSON list of the domains to be transferred.
|
||||
* Because the list of domains to process can be quite large, this action should be called by a tool
|
||||
* that batches the list of domains into reasonable sizes if necessary. The recommended usage path
|
||||
* is to call this through the {@link google.registry.tools.BulkDomainTransferCommand}, which
|
||||
* handles batching and input handling.
|
||||
*
|
||||
* <p>This runs as a single-threaded idempotent action that runs a superuser domain transfer on each
|
||||
* domain to process. We go through the standard EPP process to make sure that we have an accurate
|
||||
* historical representation of events (rather than force-modifying the domains in place).
|
||||
*
|
||||
* <p>Consider passing in an "maxQps" parameter based on the number of domains being transferred,
|
||||
* otherwise the default is {@link BatchModule#DEFAULT_MAX_QPS}.
|
||||
*/
|
||||
@Action(
|
||||
service = Action.Service.BACKEND,
|
||||
path = BulkDomainTransferAction.PATH,
|
||||
method = Action.Method.POST,
|
||||
auth = Auth.AUTH_ADMIN)
|
||||
public class BulkDomainTransferAction implements Runnable {
|
||||
|
||||
public static final String PATH = "/_dr/task/bulkDomainTransfer";
|
||||
|
||||
private static final String SUPERUSER_TRANSFER_XML_FORMAT =
|
||||
"""
|
||||
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
|
||||
<command>
|
||||
<transfer op="request">
|
||||
<domain:transfer xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
|
||||
<domain:name>%DOMAIN_NAME%</domain:name>
|
||||
</domain:transfer>
|
||||
</transfer>
|
||||
<extension>
|
||||
<superuser:domainTransferRequest xmlns:superuser="urn:google:params:xml:ns:superuser-1.0">
|
||||
<superuser:renewalPeriod unit="y">0</superuser:renewalPeriod>
|
||||
<superuser:automaticTransferLength>0</superuser:automaticTransferLength>
|
||||
</superuser:domainTransferRequest>
|
||||
<metadata:metadata xmlns:metadata="urn:google:params:xml:ns:metadata-1.0">
|
||||
<metadata:reason>%REASON%</metadata:reason>
|
||||
<metadata:requestedByRegistrar>%REQUESTED_BY_REGISTRAR%</metadata:requestedByRegistrar>
|
||||
</metadata:metadata>
|
||||
</extension>
|
||||
<clTRID>BulkDomainTransferAction</clTRID>
|
||||
</command>
|
||||
</epp>
|
||||
""";
|
||||
|
||||
private static final String LOCK_NAME = "Domain bulk transfer";
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
private final EppController eppController;
|
||||
private final LockHandler lockHandler;
|
||||
private final RateLimiter rateLimiter;
|
||||
private final ImmutableList<String> bulkTransferDomainNames;
|
||||
private final String gainingRegistrarId;
|
||||
private final String losingRegistrarId;
|
||||
private final boolean requestedByRegistrar;
|
||||
private final String reason;
|
||||
private final Response response;
|
||||
|
||||
private int successes = 0;
|
||||
private int alreadyTransferred = 0;
|
||||
private int pendingDelete = 0;
|
||||
private int missingDomains = 0;
|
||||
private int errors = 0;
|
||||
|
||||
@Inject
|
||||
BulkDomainTransferAction(
|
||||
EppController eppController,
|
||||
LockHandler lockHandler,
|
||||
@Named("standardRateLimiter") RateLimiter rateLimiter,
|
||||
@Parameter("bulkTransferDomainNames") ImmutableList<String> bulkTransferDomainNames,
|
||||
@Parameter("gainingRegistrarId") String gainingRegistrarId,
|
||||
@Parameter("losingRegistrarId") String losingRegistrarId,
|
||||
@Parameter("requestedByRegistrar") boolean requestedByRegistrar,
|
||||
@Parameter("reason") String reason,
|
||||
Response response) {
|
||||
this.eppController = eppController;
|
||||
this.lockHandler = lockHandler;
|
||||
this.rateLimiter = rateLimiter;
|
||||
this.bulkTransferDomainNames = bulkTransferDomainNames;
|
||||
this.gainingRegistrarId = gainingRegistrarId;
|
||||
this.losingRegistrarId = losingRegistrarId;
|
||||
this.requestedByRegistrar = requestedByRegistrar;
|
||||
this.reason = reason;
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
response.setContentType(PLAIN_TEXT_UTF_8);
|
||||
Callable<Void> runner =
|
||||
() -> {
|
||||
try {
|
||||
runLocked();
|
||||
response.setStatus(SC_OK);
|
||||
} catch (Exception e) {
|
||||
logger.atSevere().withCause(e).log("Errored out during execution.");
|
||||
response.setStatus(SC_INTERNAL_SERVER_ERROR);
|
||||
response.setPayload(String.format("Errored out with cause: %s", e));
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
if (!lockHandler.executeWithLocks(runner, null, Duration.standardHours(1), LOCK_NAME)) {
|
||||
// Send a 200-series status code to prevent this conflicting action from retrying.
|
||||
response.setStatus(SC_NO_CONTENT);
|
||||
response.setPayload("Could not acquire lock; already running?");
|
||||
}
|
||||
}
|
||||
|
||||
private void runLocked() {
|
||||
logger.atInfo().log("Attempting to transfer %d domains.", bulkTransferDomainNames.size());
|
||||
for (String domainName : bulkTransferDomainNames) {
|
||||
rateLimiter.acquire();
|
||||
tm().transact(() -> runTransferFlowInTransaction(domainName));
|
||||
}
|
||||
|
||||
String msg =
|
||||
String.format(
|
||||
"Finished; %d domains were successfully transferred, %d were previously transferred, %s"
|
||||
+ " were missing domains, %s are pending delete, and %d errored out.",
|
||||
successes, alreadyTransferred, missingDomains, pendingDelete, errors);
|
||||
logger.at(errors + missingDomains == 0 ? Level.INFO : Level.WARNING).log(msg);
|
||||
response.setPayload(msg);
|
||||
}
|
||||
|
||||
private void runTransferFlowInTransaction(String domainName) {
|
||||
if (shouldSkipDomain(domainName)) {
|
||||
return;
|
||||
}
|
||||
String xml =
|
||||
SUPERUSER_TRANSFER_XML_FORMAT
|
||||
.replace("%DOMAIN_NAME%", domainName)
|
||||
.replace("%REASON%", reason)
|
||||
.replace("%REQUESTED_BY_REGISTRAR%", String.valueOf(requestedByRegistrar));
|
||||
EppOutput output =
|
||||
eppController.handleEppCommand(
|
||||
new StatelessRequestSessionMetadata(
|
||||
gainingRegistrarId, ProtocolDefinition.getVisibleServiceExtensionUris()),
|
||||
new PasswordOnlyTransportCredentials(),
|
||||
EppRequestSource.TOOL,
|
||||
false,
|
||||
true,
|
||||
xml.getBytes(US_ASCII));
|
||||
if (output.isSuccess()) {
|
||||
logger.atInfo().log("Successfully transferred domain '%s'.", domainName);
|
||||
successes++;
|
||||
} else {
|
||||
logger.atWarning().log(
|
||||
"Failed transferring domain '%s' with error '%s'.",
|
||||
domainName, new String(marshalWithLenientRetry(output), US_ASCII));
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldSkipDomain(String domainName) {
|
||||
Optional<Domain> maybeDomain =
|
||||
ForeignKeyUtils.loadResource(Domain.class, domainName, tm().getTransactionTime());
|
||||
if (maybeDomain.isEmpty()) {
|
||||
logger.atWarning().log("Domain '%s' was already deleted", domainName);
|
||||
missingDomains++;
|
||||
return true;
|
||||
}
|
||||
Domain domain = maybeDomain.get();
|
||||
String currentRegistrarId = domain.getCurrentSponsorRegistrarId();
|
||||
if (currentRegistrarId.equals(gainingRegistrarId)) {
|
||||
logger.atInfo().log("Domain '%s' was already transferred", domainName);
|
||||
alreadyTransferred++;
|
||||
return true;
|
||||
}
|
||||
if (!currentRegistrarId.equals(losingRegistrarId)) {
|
||||
logger.atWarning().log(
|
||||
"Domain '%s' had unexpected registrar '%s'", domainName, currentRegistrarId);
|
||||
errors++;
|
||||
return true;
|
||||
}
|
||||
if (domain.getStatusValues().contains(StatusValue.PENDING_DELETE)
|
||||
|| !domain.getDeletionTime().equals(DateTimeUtils.END_OF_TIME)) {
|
||||
logger.atWarning().log("Domain '%s' is in PENDING_DELETE", domainName);
|
||||
pendingDelete++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -14,10 +14,8 @@
|
||||
|
||||
package google.registry.batch;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8;
|
||||
import static google.registry.flows.FlowUtils.marshalWithLenientRetry;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
import static google.registry.util.ResourceUtils.readResourceUtf8;
|
||||
@@ -36,7 +34,6 @@ import google.registry.flows.EppController;
|
||||
import google.registry.flows.EppRequestSource;
|
||||
import google.registry.flows.PasswordOnlyTransportCredentials;
|
||||
import google.registry.flows.StatelessRequestSessionMetadata;
|
||||
import google.registry.model.common.FeatureFlag;
|
||||
import google.registry.model.contact.Contact;
|
||||
import google.registry.model.domain.DesignatedContact;
|
||||
import google.registry.model.domain.Domain;
|
||||
@@ -93,7 +90,7 @@ public class RemoveAllDomainContactsAction implements Runnable {
|
||||
EppController eppController,
|
||||
@Config("registryAdminClientId") String registryAdminClientId,
|
||||
LockHandler lockHandler,
|
||||
@Named("removeAllDomainContacts") RateLimiter rateLimiter,
|
||||
@Named("standardRateLimiter") RateLimiter rateLimiter,
|
||||
Response response) {
|
||||
this.eppController = eppController;
|
||||
this.registryAdminClientId = registryAdminClientId;
|
||||
@@ -106,11 +103,7 @@ public class RemoveAllDomainContactsAction implements Runnable {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
checkState(
|
||||
tm().transact(() -> FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_PROHIBITED)),
|
||||
"Minimum dataset migration must be completed prior to running this action");
|
||||
response.setContentType(PLAIN_TEXT_UTF_8);
|
||||
|
||||
Callable<Void> runner =
|
||||
() -> {
|
||||
try {
|
||||
|
||||
@@ -36,6 +36,7 @@ import dagger.Provides;
|
||||
import google.registry.bsa.UploadBsaUnavailableDomainsAction;
|
||||
import google.registry.dns.ReadDnsRefreshRequestsAction;
|
||||
import google.registry.model.common.DnsRefreshRequest;
|
||||
import google.registry.mosapi.MosApiClient;
|
||||
import google.registry.persistence.transaction.JpaTransactionManager;
|
||||
import google.registry.request.Action.Service;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
@@ -1415,6 +1416,52 @@ public final class RegistryConfig {
|
||||
return config.bsa.uploadUnavailableDomainsUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL we send HTTP requests for MoSAPI.
|
||||
*
|
||||
* @see MosApiClient
|
||||
*/
|
||||
@Provides
|
||||
@Config("mosapiServiceUrl")
|
||||
public static String provideMosapiServiceUrl(RegistryConfigSettings config) {
|
||||
return config.mosapi.serviceUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the entityType we send HTTP requests for MoSAPI.
|
||||
*
|
||||
* @see MosApiClient
|
||||
*/
|
||||
@Provides
|
||||
@Config("mosapiEntityType")
|
||||
public static String provideMosapiEntityType(RegistryConfigSettings config) {
|
||||
return config.mosapi.entityType;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Config("mosapiTlsCertSecretName")
|
||||
public static String provideMosapiTlsCertSecretName(RegistryConfigSettings config) {
|
||||
return config.mosapi.tlsCertSecretName;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Config("mosapiTlsKeySecretName")
|
||||
public static String provideMosapiTlsKeySecretName(RegistryConfigSettings config) {
|
||||
return config.mosapi.tlsKeySecretName;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Config("mosapiTlds")
|
||||
public static ImmutableSet<String> provideMosapiTlds(RegistryConfigSettings config) {
|
||||
return ImmutableSet.copyOf(config.mosapi.tlds);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Config("mosapiServices")
|
||||
public static ImmutableSet<String> provideMosapiServices(RegistryConfigSettings config) {
|
||||
return ImmutableSet.copyOf(config.mosapi.services);
|
||||
}
|
||||
|
||||
private static String formatComments(String text) {
|
||||
return Splitter.on('\n').omitEmptyStrings().trimResults().splitToList(text).stream()
|
||||
.map(s -> "# " + s)
|
||||
|
||||
@@ -43,6 +43,7 @@ public class RegistryConfigSettings {
|
||||
public DnsUpdate dnsUpdate;
|
||||
public BulkPricingPackageMonitoring bulkPricingPackageMonitoring;
|
||||
public Bsa bsa;
|
||||
public MosApi mosapi;
|
||||
|
||||
/** Configuration options that apply to the entire GCP project. */
|
||||
public static class GcpProject {
|
||||
@@ -262,4 +263,14 @@ public class RegistryConfigSettings {
|
||||
public String unblockableDomainsUrl;
|
||||
public String uploadUnavailableDomainsUrl;
|
||||
}
|
||||
|
||||
/** Configuration for Mosapi. */
|
||||
public static class MosApi {
|
||||
public String serviceUrl;
|
||||
public String tlsCertSecretName;
|
||||
public String tlsKeySecretName;
|
||||
public String entityType;
|
||||
public List<String> tlds;
|
||||
public List<String> services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -616,3 +616,30 @@ bsa:
|
||||
unblockableDomainsUrl: "https://"
|
||||
# API endpoint for uploading the list of unavailable domain names.
|
||||
uploadUnavailableDomainsUrl: "https://"
|
||||
|
||||
mosapi:
|
||||
# URL for the MosAPI
|
||||
serviceUrl: https://mosapi.icann.org
|
||||
# The type of entity being monitored.
|
||||
# For registries, this is 'ry'
|
||||
# For registrars, this is 'rr'
|
||||
entityType: ry
|
||||
# Add your List of TLDs to be monitored
|
||||
tlds:
|
||||
- your_tld1
|
||||
- your_tld2
|
||||
# Add tls cert secret name
|
||||
# you configured in secret manager
|
||||
tlsCertSecretName: YOUR_TLS_CERT_SECRET_NAME
|
||||
# Add tls key secret name
|
||||
# you configured in secret manager
|
||||
tlsKeySecretName: YOUR_TLS_KEY_SECRET_NAME
|
||||
# List of services to check for each TLD.
|
||||
services:
|
||||
- "dns"
|
||||
- "rdap"
|
||||
- "rdds"
|
||||
- "epp"
|
||||
- "dnssec"
|
||||
|
||||
|
||||
|
||||
@@ -186,12 +186,9 @@ import org.joda.time.Duration;
|
||||
* @error {@link DomainFlowUtils.LinkedResourceInPendingDeleteProhibitsOperationException}
|
||||
* @error {@link DomainFlowUtils.MalformedTcnIdException}
|
||||
* @error {@link DomainFlowUtils.MaxSigLifeNotSupportedException}
|
||||
* @error {@link DomainFlowUtils.MissingAdminContactException}
|
||||
* @error {@link DomainFlowUtils.MissingBillingAccountMapException}
|
||||
* @error {@link DomainFlowUtils.MissingClaimsNoticeException}
|
||||
* @error {@link DomainFlowUtils.MissingContactTypeException}
|
||||
* @error {@link DomainFlowUtils.MissingRegistrantException}
|
||||
* @error {@link DomainFlowUtils.MissingTechnicalContactException}
|
||||
* @error {@link DomainFlowUtils.NameserversNotAllowedForTldException}
|
||||
* @error {@link DomainFlowUtils.NameserversNotSpecifiedForTldWithNameserverAllowListException}
|
||||
* @error {@link DomainFlowUtils.PremiumNameBlockedException}
|
||||
|
||||
@@ -24,8 +24,6 @@ import static com.google.common.collect.Sets.difference;
|
||||
import static com.google.common.collect.Sets.intersection;
|
||||
import static com.google.common.collect.Sets.union;
|
||||
import static google.registry.bsa.persistence.BsaLabelUtils.isLabelBlocked;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED;
|
||||
import static google.registry.model.domain.Domain.MAX_REGISTRATION_YEARS;
|
||||
import static google.registry.model.domain.token.AllocationToken.TokenType.REGISTER_BSA;
|
||||
import static google.registry.model.tld.Tld.TldState.GENERAL_AVAILABILITY;
|
||||
@@ -81,7 +79,6 @@ import google.registry.model.EppResource;
|
||||
import google.registry.model.billing.BillingBase.Flag;
|
||||
import google.registry.model.billing.BillingBase.Reason;
|
||||
import google.registry.model.billing.BillingRecurrence;
|
||||
import google.registry.model.common.FeatureFlag;
|
||||
import google.registry.model.contact.Contact;
|
||||
import google.registry.model.domain.DesignatedContact;
|
||||
import google.registry.model.domain.DesignatedContact.Type;
|
||||
@@ -138,7 +135,6 @@ import google.registry.util.Idn;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
@@ -486,31 +482,12 @@ public class DomainFlowUtils {
|
||||
*/
|
||||
static void validateCreateContactData(
|
||||
Optional<VKey<Contact>> registrant, Set<DesignatedContact> contacts)
|
||||
throws RequiredParameterMissingException, ParameterValuePolicyErrorException {
|
||||
// TODO(b/353347632): Change these flag checks to a registry config check once minimum data set
|
||||
// migration is completed.
|
||||
if (FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_PROHIBITED)) {
|
||||
if (registrant.isPresent()) {
|
||||
throw new RegistrantProhibitedException();
|
||||
}
|
||||
if (!contacts.isEmpty()) {
|
||||
throw new ContactsProhibitedException();
|
||||
}
|
||||
} else if (!FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_OPTIONAL)) {
|
||||
if (registrant.isEmpty()) {
|
||||
throw new MissingRegistrantException();
|
||||
}
|
||||
|
||||
Set<Type> roles = new HashSet<>();
|
||||
for (DesignatedContact contact : contacts) {
|
||||
roles.add(contact.getType());
|
||||
}
|
||||
if (!roles.contains(Type.ADMIN)) {
|
||||
throw new MissingAdminContactException();
|
||||
}
|
||||
if (!roles.contains(Type.TECH)) {
|
||||
throw new MissingTechnicalContactException();
|
||||
}
|
||||
throws ParameterValuePolicyErrorException {
|
||||
if (registrant.isPresent()) {
|
||||
throw new RegistrantProhibitedException();
|
||||
}
|
||||
if (!contacts.isEmpty()) {
|
||||
throw new ContactsProhibitedException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -523,33 +500,14 @@ public class DomainFlowUtils {
|
||||
Optional<VKey<Contact>> newRegistrant,
|
||||
Set<DesignatedContact> existingContacts,
|
||||
Set<DesignatedContact> newContacts)
|
||||
throws RequiredParameterMissingException, ParameterValuePolicyErrorException {
|
||||
// TODO(b/353347632): Change these flag checks to a registry config check once minimum data set
|
||||
// migration is completed.
|
||||
if (FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_PROHIBITED)) {
|
||||
// Throw if the update specifies a new registrant that is different from the existing one.
|
||||
if (newRegistrant.isPresent() && !newRegistrant.equals(existingRegistrant)) {
|
||||
throw new RegistrantProhibitedException();
|
||||
}
|
||||
// Throw if the update specifies any new contacts that weren't already present on the domain.
|
||||
if (!Sets.difference(newContacts, existingContacts).isEmpty()) {
|
||||
throw new ContactsProhibitedException();
|
||||
}
|
||||
} else if (!FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_OPTIONAL)) {
|
||||
// Throw if the update empties out a registrant that had been present.
|
||||
if (newRegistrant.isEmpty() && existingRegistrant.isPresent()) {
|
||||
throw new MissingRegistrantException();
|
||||
}
|
||||
// Throw if the update contains no admin contact when one had been present.
|
||||
if (existingContacts.stream().anyMatch(c -> c.getType().equals(Type.ADMIN))
|
||||
&& newContacts.stream().noneMatch(c -> c.getType().equals(Type.ADMIN))) {
|
||||
throw new MissingAdminContactException();
|
||||
}
|
||||
// Throw if the update contains no tech contact when one had been present.
|
||||
if (existingContacts.stream().anyMatch(c -> c.getType().equals(Type.TECH))
|
||||
&& newContacts.stream().noneMatch(c -> c.getType().equals(Type.TECH))) {
|
||||
throw new MissingTechnicalContactException();
|
||||
}
|
||||
throws ParameterValuePolicyErrorException {
|
||||
// Throw if the update specifies a new registrant that is different from the existing one.
|
||||
if (newRegistrant.isPresent() && !newRegistrant.equals(existingRegistrant)) {
|
||||
throw new RegistrantProhibitedException();
|
||||
}
|
||||
// Throw if the update specifies any new contacts that weren't already present on the domain.
|
||||
if (!Sets.difference(newContacts, existingContacts).isEmpty()) {
|
||||
throw new ContactsProhibitedException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1398,13 +1356,6 @@ public class DomainFlowUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/** Registrant is required. */
|
||||
static class MissingRegistrantException extends RequiredParameterMissingException {
|
||||
public MissingRegistrantException() {
|
||||
super("Registrant is required");
|
||||
}
|
||||
}
|
||||
|
||||
/** Having a registrant is prohibited by registry policy. */
|
||||
static class RegistrantProhibitedException extends ParameterValuePolicyErrorException {
|
||||
public RegistrantProhibitedException() {
|
||||
@@ -1412,20 +1363,6 @@ public class DomainFlowUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/** Admin contact is required. */
|
||||
static class MissingAdminContactException extends RequiredParameterMissingException {
|
||||
public MissingAdminContactException() {
|
||||
super("Admin contact is required");
|
||||
}
|
||||
}
|
||||
|
||||
/** Technical contact is required. */
|
||||
static class MissingTechnicalContactException extends RequiredParameterMissingException {
|
||||
public MissingTechnicalContactException() {
|
||||
super("Technical contact is required");
|
||||
}
|
||||
}
|
||||
|
||||
/** Too many nameservers set on this domain. */
|
||||
static class TooManyNameserversException extends ParameterValuePolicyErrorException {
|
||||
public TooManyNameserversException(String message) {
|
||||
|
||||
@@ -133,10 +133,9 @@ import org.joda.time.DateTime;
|
||||
@ReportingSpec(ActivityReportField.DOMAIN_TRANSFER_REQUEST)
|
||||
public final class DomainTransferRequestFlow implements MutatingFlow {
|
||||
|
||||
private static final ImmutableSet<StatusValue> DISALLOWED_STATUSES = ImmutableSet.of(
|
||||
StatusValue.CLIENT_TRANSFER_PROHIBITED,
|
||||
StatusValue.PENDING_DELETE,
|
||||
StatusValue.SERVER_TRANSFER_PROHIBITED);
|
||||
private static final ImmutableSet<StatusValue> NON_SUPERUSER_DISALLOWED_STATUSES =
|
||||
ImmutableSet.of(
|
||||
StatusValue.CLIENT_TRANSFER_PROHIBITED, StatusValue.SERVER_TRANSFER_PROHIBITED);
|
||||
|
||||
@Inject ResourceCommand resourceCommand;
|
||||
@Inject ExtensionManager extensionManager;
|
||||
@@ -299,8 +298,9 @@ public final class DomainTransferRequestFlow implements MutatingFlow {
|
||||
DateTime now,
|
||||
Optional<DomainTransferRequestSuperuserExtension> superuserExtension)
|
||||
throws EppException {
|
||||
verifyNoDisallowedStatuses(existingDomain, DISALLOWED_STATUSES);
|
||||
verifyNoDisallowedStatuses(existingDomain, ImmutableSet.of(StatusValue.PENDING_DELETE));
|
||||
if (!isSuperuser) {
|
||||
verifyNoDisallowedStatuses(existingDomain, NON_SUPERUSER_DISALLOWED_STATUSES);
|
||||
verifyAuthInfoPresentForResourceTransfer(authInfo);
|
||||
verifyAuthInfo(authInfo.get(), existingDomain);
|
||||
}
|
||||
|
||||
@@ -39,8 +39,6 @@ import static google.registry.flows.domain.DomainFlowUtils.validateNoDuplicateCo
|
||||
import static google.registry.flows.domain.DomainFlowUtils.validateUpdateContactData;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.verifyClientUpdateNotProhibited;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.verifyNotInPendingDelete;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED;
|
||||
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_UPDATE;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
@@ -61,13 +59,11 @@ import google.registry.flows.custom.DomainUpdateFlowCustomLogic;
|
||||
import google.registry.flows.custom.DomainUpdateFlowCustomLogic.AfterValidationParameters;
|
||||
import google.registry.flows.custom.DomainUpdateFlowCustomLogic.BeforeSaveParameters;
|
||||
import google.registry.flows.custom.EntityChanges;
|
||||
import google.registry.flows.domain.DomainFlowUtils.MissingRegistrantException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.NameserversNotSpecifiedForTldWithNameserverAllowListException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.RegistrantProhibitedException;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.billing.BillingBase.Reason;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.common.FeatureFlag;
|
||||
import google.registry.model.contact.Contact;
|
||||
import google.registry.model.domain.DesignatedContact;
|
||||
import google.registry.model.domain.Domain;
|
||||
@@ -123,10 +119,7 @@ import org.joda.time.DateTime;
|
||||
* @error {@link DomainFlowUtils.LinkedResourcesDoNotExistException}
|
||||
* @error {@link DomainFlowUtils.LinkedResourceInPendingDeleteProhibitsOperationException}
|
||||
* @error {@link DomainFlowUtils.MaxSigLifeChangeNotSupportedException}
|
||||
* @error {@link DomainFlowUtils.MissingAdminContactException}
|
||||
* @error {@link DomainFlowUtils.MissingContactTypeException}
|
||||
* @error {@link DomainFlowUtils.MissingTechnicalContactException}
|
||||
* @error {@link DomainFlowUtils.MissingRegistrantException}
|
||||
* @error {@link DomainFlowUtils.NameserversNotAllowedForTldException}
|
||||
* @error {@link NameserversNotSpecifiedForTldWithNameserverAllowListException}
|
||||
* @error {@link DomainFlowUtils.NotAuthorizedForTldException}
|
||||
@@ -307,18 +300,11 @@ public final class DomainUpdateFlow implements MutatingFlow {
|
||||
return domainBuilder.build();
|
||||
}
|
||||
|
||||
private Optional<VKey<Contact>> determineUpdatedRegistrant(Change change, Domain domain)
|
||||
throws EppException {
|
||||
private Optional<VKey<Contact>> determineUpdatedRegistrant(Change change, Domain domain) {
|
||||
// During or after the minimum dataset transition, allow registrant to be removed.
|
||||
if (change.getRegistrantContactId().isPresent()
|
||||
&& change.getRegistrantContactId().get().isEmpty()) {
|
||||
// TODO(b/353347632): Change this flag check to a registry config check.
|
||||
if (FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_OPTIONAL)
|
||||
|| FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_PROHIBITED)) {
|
||||
return Optional.empty();
|
||||
} else {
|
||||
throw new MissingRegistrantException();
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
return change.getRegistrant().or(domain::getRegistrant);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import google.registry.keyring.api.KeyModule;
|
||||
import google.registry.module.RegistryComponent.RegistryModule;
|
||||
import google.registry.module.RequestComponent.RequestComponentModule;
|
||||
import google.registry.monitoring.whitebox.StackdriverModule;
|
||||
import google.registry.mosapi.module.MosApiModule;
|
||||
import google.registry.persistence.PersistenceModule;
|
||||
import google.registry.privileges.secretmanager.SecretManagerModule;
|
||||
import google.registry.rde.JSchModule;
|
||||
@@ -71,6 +72,7 @@ import jakarta.inject.Singleton;
|
||||
GroupsModule.class,
|
||||
GroupssettingsModule.class,
|
||||
GsonModule.class,
|
||||
MosApiModule.class,
|
||||
JSchModule.class,
|
||||
KeyModule.class,
|
||||
KeyringModule.class,
|
||||
|
||||
@@ -17,6 +17,7 @@ package google.registry.module;
|
||||
import dagger.Module;
|
||||
import dagger.Subcomponent;
|
||||
import google.registry.batch.BatchModule;
|
||||
import google.registry.batch.BulkDomainTransferAction;
|
||||
import google.registry.batch.CannedScriptExecutionAction;
|
||||
import google.registry.batch.DeleteExpiredDomainsAction;
|
||||
import google.registry.batch.DeleteLoadTestDataAction;
|
||||
@@ -171,6 +172,8 @@ interface RequestComponent {
|
||||
|
||||
BsaValidateAction bsaValidateAction();
|
||||
|
||||
BulkDomainTransferAction bulkDomainTransferAction();
|
||||
|
||||
CannedScriptExecutionAction cannedScriptExecutionAction();
|
||||
|
||||
CheckApiAction checkApiAction();
|
||||
|
||||
150
core/src/main/java/google/registry/mosapi/MosApiClient.java
Normal file
150
core/src/main/java/google/registry/mosapi/MosApiClient.java
Normal file
@@ -0,0 +1,150 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.mosapi;
|
||||
|
||||
import static org.apache.beam.sdk.util.Preconditions.checkArgumentNotNull;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.mosapi.MosApiException.MosApiAuthorizationException;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
import jakarta.inject.Singleton;
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.util.Map;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
@Singleton
|
||||
public class MosApiClient {
|
||||
|
||||
private final OkHttpClient httpClient;
|
||||
private final String baseUrl;
|
||||
|
||||
@Inject
|
||||
public MosApiClient(
|
||||
@Named("mosapiHttpClient") OkHttpClient httpClient,
|
||||
@Config("mosapiServiceUrl") String mosapiUrl,
|
||||
@Config("mosapiEntityType") String entityType) {
|
||||
this.httpClient = httpClient;
|
||||
// Pre-calculate base URL and validate it to fail fast on bad config
|
||||
String fullUrl = String.format("%s/%s", mosapiUrl, entityType);
|
||||
checkArgumentNotNull(
|
||||
HttpUrl.parse(fullUrl), "Invalid MoSAPI Service URL configuration: %s", fullUrl);
|
||||
|
||||
this.baseUrl = fullUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a GET request to the specified MoSAPI endpoint.
|
||||
*
|
||||
* @param entityId The TLD or registrar ID the request is for.
|
||||
* @param endpoint The specific API endpoint path (e.g., "v2/monitoring/state").
|
||||
* @param params A map of query parameters to be URL-encoded and appended to the request.
|
||||
* @param headers A map of HTTP headers to be included in the request.
|
||||
* @return The {@link Response} from the server if the request is successful. <b>The caller is
|
||||
* responsible for closing this response.</b>
|
||||
* @throws MosApiException if the request fails due to a network error or an unhandled HTTP
|
||||
* status.
|
||||
* @throws MosApiAuthorizationException if the server returns a 401 Unauthorized status.
|
||||
*/
|
||||
public Response sendGetRequest(
|
||||
String entityId, String endpoint, Map<String, String> params, Map<String, String> headers)
|
||||
throws MosApiException {
|
||||
HttpUrl url = buildUri(entityId, endpoint, params);
|
||||
Request.Builder requestBuilder = new Request.Builder().url(url).get();
|
||||
headers.forEach(requestBuilder::addHeader);
|
||||
try {
|
||||
Response response = httpClient.newCall(requestBuilder.build()).execute();
|
||||
return checkResponseForAuthError(response);
|
||||
} catch (RuntimeException | IOException e) {
|
||||
// Check if it's the specific authorization exception (re-thrown or caught here)
|
||||
Throwables.throwIfInstanceOf(e, MosApiAuthorizationException.class);
|
||||
// Otherwise, treat as a generic connection/API error
|
||||
throw new MosApiException("Error during GET request to " + url, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a POST request to the specified MoSAPI endpoint.
|
||||
*
|
||||
* <p><b>Note:</b> This method is for future use. There are currently no MoSAPI endpoints in the
|
||||
* project scope that require a POST request.
|
||||
*
|
||||
* @param entityId The TLD or registrar ID the request is for.
|
||||
* @param endpoint The specific API endpoint path.
|
||||
* @param params A map of query parameters to be URL-encoded.
|
||||
* @param headers A map of HTTP headers to be included in the request.
|
||||
* @param body The request body to be sent with the POST request.
|
||||
* @return The {@link Response} from the server. <b>The caller is responsible for closing this
|
||||
* response.</b>
|
||||
* @throws MosApiException if the request fails.
|
||||
* @throws MosApiAuthorizationException if the server returns a 401 Unauthorized status.
|
||||
*/
|
||||
public Response sendPostRequest(
|
||||
String entityId,
|
||||
String endpoint,
|
||||
Map<String, String> params,
|
||||
Map<String, String> headers,
|
||||
String body)
|
||||
throws MosApiException {
|
||||
HttpUrl url = buildUri(entityId, endpoint, params);
|
||||
RequestBody requestBody = RequestBody.create(body, MediaType.parse("application/json"));
|
||||
|
||||
Request.Builder requestBuilder = new Request.Builder().url(url).post(requestBody);
|
||||
headers.forEach(requestBuilder::addHeader);
|
||||
try {
|
||||
Response response = httpClient.newCall(requestBuilder.build()).execute();
|
||||
return checkResponseForAuthError(response);
|
||||
} catch (RuntimeException | IOException e) {
|
||||
// Check if it's the specific authorization exception (re-thrown or caught here)
|
||||
Throwables.throwIfInstanceOf(e, MosApiAuthorizationException.class);
|
||||
// Otherwise, treat as a generic connection/API error
|
||||
throw new MosApiException("Error during POST request to " + url, e);
|
||||
}
|
||||
}
|
||||
|
||||
private Response checkResponseForAuthError(Response response)
|
||||
throws MosApiAuthorizationException {
|
||||
if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
|
||||
response.close();
|
||||
throw new MosApiAuthorizationException(
|
||||
"Authorization failed for the requested resource. The client certificate may not be"
|
||||
+ " authorized for the specified TLD or Registrar.");
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the full URL for a request, including the base URL, entityId, path, and query params.
|
||||
*/
|
||||
private HttpUrl buildUri(String entityId, String path, Map<String, String> queryParams) {
|
||||
String sanitizedPath = path.startsWith("/") ? path.substring(1) : path;
|
||||
|
||||
// We can safely use get() here because we validated baseUrl in the constructor
|
||||
HttpUrl.Builder urlBuilder =
|
||||
HttpUrl.get(baseUrl).newBuilder().addPathSegment(entityId).addPathSegments(sanitizedPath);
|
||||
|
||||
if (queryParams != null) {
|
||||
queryParams.forEach(urlBuilder::addQueryParameter);
|
||||
}
|
||||
return urlBuilder.build();
|
||||
}
|
||||
}
|
||||
110
core/src/main/java/google/registry/mosapi/MosApiException.java
Normal file
110
core/src/main/java/google/registry/mosapi/MosApiException.java
Normal file
@@ -0,0 +1,110 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.mosapi;
|
||||
|
||||
import static java.lang.annotation.ElementType.TYPE;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
import google.registry.mosapi.model.MosApiErrorResponse;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Custom exception for MoSAPI client errors. */
|
||||
public class MosApiException extends IOException {
|
||||
|
||||
private final MosApiErrorResponse errorResponse;
|
||||
|
||||
public MosApiException(MosApiErrorResponse errorResponse) {
|
||||
super(
|
||||
String.format(
|
||||
"MoSAPI returned an error (code: %s): %s",
|
||||
errorResponse.resultCode(), errorResponse.message()));
|
||||
this.errorResponse = errorResponse;
|
||||
}
|
||||
|
||||
public MosApiException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.errorResponse = null;
|
||||
}
|
||||
|
||||
public Optional<MosApiErrorResponse> getErrorResponse() {
|
||||
return Optional.ofNullable(errorResponse);
|
||||
}
|
||||
|
||||
/** Annotation for associating a MoSAPI result code with an exception subclass. */
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
@Target(TYPE)
|
||||
public @interface MosApiResultCode {
|
||||
String value();
|
||||
}
|
||||
|
||||
/** Thrown when MoSAPI returns a 401 Unauthorized error. */
|
||||
public static class MosApiAuthorizationException extends MosApiException {
|
||||
public MosApiAuthorizationException(String message) {
|
||||
super(message, null);
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates a specific exception based on the MoSAPI error response. */
|
||||
public static MosApiException create(MosApiErrorResponse errorResponse) {
|
||||
Optional<MosApiResponse> responseEnum = MosApiResponse.fromCode(errorResponse.resultCode());
|
||||
if (responseEnum.isPresent()) {
|
||||
return switch (responseEnum.get()) {
|
||||
case DATE_DURATION_INVALID -> new DateDurationInvalidException(errorResponse);
|
||||
case DATE_ORDER_INVALID -> new DateOrderInvalidException(errorResponse);
|
||||
case START_DATE_SYNTAX_INVALID -> new StartDateSyntaxInvalidException(errorResponse);
|
||||
case END_DATE_SYNTAX_INVALID -> new EndDateSyntaxInvalidException(errorResponse);
|
||||
default -> new MosApiException(errorResponse);
|
||||
};
|
||||
}
|
||||
return new MosApiException(errorResponse);
|
||||
}
|
||||
|
||||
/** Thrown when the date duration in a MoSAPI request is invalid. */
|
||||
@MosApiResultCode("2011")
|
||||
public static class DateDurationInvalidException extends MosApiException {
|
||||
public DateDurationInvalidException(MosApiErrorResponse errorResponse) {
|
||||
super(errorResponse);
|
||||
}
|
||||
}
|
||||
|
||||
/** Thrown when the date order in a MoSAPI request is invalid. */
|
||||
@MosApiResultCode("2012")
|
||||
public static class DateOrderInvalidException extends MosApiException {
|
||||
public DateOrderInvalidException(MosApiErrorResponse errorResponse) {
|
||||
super(errorResponse);
|
||||
}
|
||||
}
|
||||
|
||||
/** Thrown when the startDate syntax in a MoSAPI request is invalid. */
|
||||
@MosApiResultCode("2013")
|
||||
public static class StartDateSyntaxInvalidException extends MosApiException {
|
||||
public StartDateSyntaxInvalidException(MosApiErrorResponse errorResponse) {
|
||||
super(errorResponse);
|
||||
}
|
||||
}
|
||||
|
||||
/** Thrown when the endDate syntax in a MoSAPI request is invalid. */
|
||||
@MosApiResultCode("2014")
|
||||
public static class EndDateSyntaxInvalidException extends MosApiException {
|
||||
public EndDateSyntaxInvalidException(MosApiErrorResponse errorResponse) {
|
||||
super(errorResponse);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.mosapi;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Represents known MoSAPI API result codes and their default messages.
|
||||
*
|
||||
* <p>The definitions for these codes can be found in the official ICANN MoSAPI Specification,
|
||||
* specifically in the 'Result Codes' section.
|
||||
*
|
||||
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Specification</a>
|
||||
*/
|
||||
public enum MosApiResponse {
|
||||
DATE_DURATION_INVALID(
|
||||
"2011", "The difference between endDate and startDate is " + "more than 31 days"),
|
||||
DATE_ORDER_INVALID("2012", "The EndDate is before startDate"),
|
||||
START_DATE_SYNTAX_INVALID("2013", "StartDate syntax is invalid"),
|
||||
END_DATE_SYNTAX_INVALID("2014", "EndDate syntax is invalid");
|
||||
|
||||
private final String code;
|
||||
private final String defaultMessage;
|
||||
|
||||
private static final Map<String, MosApiResponse> CODE_MAP =
|
||||
Arrays.stream(values()).collect(Collectors.toMap(e -> e.code, Function.identity()));
|
||||
|
||||
MosApiResponse(String code, String defaultMessage) {
|
||||
this.code = code;
|
||||
this.defaultMessage = defaultMessage;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getDefaultMessage() {
|
||||
return defaultMessage;
|
||||
}
|
||||
|
||||
// Returns the enum constant associated with the given result code string
|
||||
public static Optional<MosApiResponse> fromCode(String code) {
|
||||
|
||||
return Optional.ofNullable(CODE_MAP.get(code));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.mosapi.model;
|
||||
|
||||
/**
|
||||
* Represents the generic JSON error response from the MoSAPI service for a 400 Bad Request.
|
||||
*
|
||||
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Specification, Section
|
||||
* 8</a>
|
||||
*/
|
||||
public record MosApiErrorResponse(String resultCode, String message, String description) {}
|
||||
@@ -0,0 +1,187 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.mosapi.module;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.privileges.secretmanager.SecretManagerClient;
|
||||
import jakarta.inject.Named;
|
||||
import jakarta.inject.Provider;
|
||||
import jakarta.inject.Singleton;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.util.Optional;
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
import okhttp3.OkHttpClient;
|
||||
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
|
||||
import org.bouncycastle.openssl.PEMKeyPair;
|
||||
import org.bouncycastle.openssl.PEMParser;
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
|
||||
|
||||
@Module
|
||||
public final class MosApiModule {
|
||||
|
||||
// Secret Manager constants
|
||||
private static final String LATEST_SECRET_VERSION = "latest";
|
||||
|
||||
// @Named annotations for Dagger
|
||||
private static final String MOSAPI_TLS_CERT = "mosapiTlsCert";
|
||||
private static final String MOSAPI_TLS_KEY = "mosapiTlsKey";
|
||||
private static final String MOSAPI_SSL_CONTEXT = "mosapiSslContext";
|
||||
private static final String MOSAPI_HTTP_CLIENT = "mosapiHttpClient";
|
||||
|
||||
// Cryptography-related constants
|
||||
private static final String CERTIFICATE_TYPE = "X.509";
|
||||
private static final String KEY_STORE_TYPE = "PKCS12";
|
||||
private static final String KEY_STORE_ALIAS = "client";
|
||||
private static final String SSL_CONTEXT_PROTOCOL = "TLS";
|
||||
|
||||
/**
|
||||
* Provides a Provider for the MoSAPI TLS Cert.
|
||||
*
|
||||
* <p>This method returns a Dagger {@link Provider} that can be used to fetch the TLS Certs for a
|
||||
* MosAPI.
|
||||
*
|
||||
* @param secretManagerClient The injected Secret Manager client.
|
||||
* @param tlsCertSecretName The name of the secret in Secret Manager (from config).
|
||||
* @return A Provider for the MoSAPI TLS Certs.
|
||||
*/
|
||||
@Provides
|
||||
@Named(MOSAPI_TLS_CERT)
|
||||
public static String provideMosapiTlsCert(
|
||||
SecretManagerClient secretManagerClient,
|
||||
@Config("mosapiTlsCertSecretName") String tlsCertSecretName) {
|
||||
return secretManagerClient.getSecretData(tlsCertSecretName, Optional.of(LATEST_SECRET_VERSION));
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a Provider for the MoSAPI TLS Key.
|
||||
*
|
||||
* <p>This method returns a Dagger {@link Provider} that can be used to fetch the TLS Key for a
|
||||
* MosAPI.
|
||||
*
|
||||
* @param secretManagerClient The injected Secret Manager client.
|
||||
* @param tlsKeySecretName The name of the secret in Secret Manager (from config).
|
||||
* @return A Provider for the MoSAPI TLS Key.
|
||||
*/
|
||||
@Provides
|
||||
@Named(MOSAPI_TLS_KEY)
|
||||
public static String provideMosapiTlsKey(
|
||||
SecretManagerClient secretManagerClient,
|
||||
@Config("mosapiTlsKeySecretName") String tlsKeySecretName) {
|
||||
return secretManagerClient.getSecretData(tlsKeySecretName, Optional.of(LATEST_SECRET_VERSION));
|
||||
}
|
||||
|
||||
@Provides
|
||||
static Certificate provideCertificate(@Named(MOSAPI_TLS_CERT) String tlsCert) {
|
||||
try {
|
||||
CertificateFactory cf = CertificateFactory.getInstance(CERTIFICATE_TYPE);
|
||||
return cf.generateCertificate(
|
||||
new ByteArrayInputStream(tlsCert.getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (CertificateException e) {
|
||||
throw new RuntimeException("Could not create X.509 certificate from provided PEM", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
static PrivateKey providePrivateKey(@Named(MOSAPI_TLS_KEY) String tlsKey) {
|
||||
try (PEMParser pemParser = new PEMParser(new StringReader(tlsKey))) {
|
||||
Object parsedObj = pemParser.readObject();
|
||||
JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
|
||||
if (parsedObj instanceof PEMKeyPair) {
|
||||
return converter.getPrivateKey(((PEMKeyPair) parsedObj).getPrivateKeyInfo());
|
||||
} else if (parsedObj instanceof PrivateKeyInfo) {
|
||||
return converter.getPrivateKey((PrivateKeyInfo) parsedObj);
|
||||
}
|
||||
throw new IllegalArgumentException(
|
||||
String.format(
|
||||
"Could not parse TLS private key; unexpected format %s",
|
||||
parsedObj != null ? parsedObj.getClass().getName() : "null"));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Could not parse TLS private key from PEM string", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
static KeyStore provideKeyStore(PrivateKey privateKey, Certificate certificate) {
|
||||
try {
|
||||
KeyStore keyStore = KeyStore.getInstance(KEY_STORE_TYPE);
|
||||
keyStore.load(null, null);
|
||||
keyStore.setKeyEntry(
|
||||
KEY_STORE_ALIAS, privateKey, new char[0], new Certificate[] {certificate});
|
||||
return keyStore;
|
||||
} catch (GeneralSecurityException | IOException e) {
|
||||
throw new RuntimeException("Could not create KeyStore for mTLS", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
static KeyManagerFactory provideKeyManagerFactory(KeyStore keyStore) {
|
||||
try {
|
||||
KeyManagerFactory kmf =
|
||||
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
|
||||
kmf.init(keyStore, new char[0]);
|
||||
return kmf;
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new RuntimeException("Could not initialize KeyManagerFactory", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named(MOSAPI_SSL_CONTEXT)
|
||||
static SSLContext provideSslContext(KeyManagerFactory keyManagerFactory) {
|
||||
try {
|
||||
SSLContext sslContext = SSLContext.getInstance(SSL_CONTEXT_PROTOCOL);
|
||||
sslContext.init(keyManagerFactory.getKeyManagers(), null, null);
|
||||
return sslContext;
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new RuntimeException("Could not initialize SSLContext", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
static X509TrustManager provideTrustManager() {
|
||||
try {
|
||||
TrustManagerFactory trustManagerFactory =
|
||||
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
trustManagerFactory.init((KeyStore) null);
|
||||
return (X509TrustManager) trustManagerFactory.getTrustManagers()[0];
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new RuntimeException("Could not initialize TrustManager", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named(MOSAPI_HTTP_CLIENT)
|
||||
static OkHttpClient provideMosapiHttpClient(
|
||||
@Named(MOSAPI_SSL_CONTEXT) SSLContext sslContext, X509TrustManager trustManager) {
|
||||
return new OkHttpClient.Builder()
|
||||
.sslSocketFactory(sslContext.getSocketFactory(), trustManager)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
16
core/src/main/java/google/registry/mosapi/package-info.java
Normal file
16
core/src/main/java/google/registry/mosapi/package-info.java
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
@javax.annotation.ParametersAreNonnullByDefault
|
||||
package google.registry.mosapi;
|
||||
@@ -0,0 +1,158 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.tools;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.beust.jcommander.Parameter;
|
||||
import com.beust.jcommander.Parameters;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.io.Files;
|
||||
import com.google.common.net.MediaType;
|
||||
import com.google.gson.Gson;
|
||||
import google.registry.batch.BulkDomainTransferAction;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.util.DomainNameUtils;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A command to bulk-transfer any number of domains from one registrar to another.
|
||||
*
|
||||
* <p>This should be used as part of the BTAPPA (Bulk Transfer After a Partial Portfolio
|
||||
* Acquisition) process in order to transfer a (possibly large) list of domains from one registrar
|
||||
* to another, though it may be used in other situations as well.
|
||||
*
|
||||
* <p>For a true bulk transfer of domains, one should pass in a file with a list of domains (one per
|
||||
* line) but if we need to do an ad-hoc transfer of one domain we can do that as well.
|
||||
*
|
||||
* <p>For BTAPPA purposes, we expect "requestedByRegistrar" to be true; this may not be the case for
|
||||
* other purposes e.g. legal compliance transfers.
|
||||
*/
|
||||
@Parameters(
|
||||
separators = " =",
|
||||
commandDescription = "Transfer domain(s) in bulk with immediate effect.")
|
||||
public class BulkDomainTransferCommand extends ConfirmingCommand implements CommandWithConnection {
|
||||
|
||||
// we don't need any configuration on the Gson because all we need is a list of strings
|
||||
private static final Gson GSON = new Gson();
|
||||
private static final int DOMAIN_TRANSFER_BATCH_SIZE = 1000;
|
||||
|
||||
@Parameter(
|
||||
names = {"--domains"},
|
||||
description =
|
||||
"Comma-separated list of domains to transfer, otherwise use --domain_names_file to"
|
||||
+ " specify a possibly-large list of domains")
|
||||
private List<String> domains;
|
||||
|
||||
@Parameter(
|
||||
names = {"-d", "--domain_names_file"},
|
||||
description = "A file with a list of newline-delimited domain names to create tokens for")
|
||||
private String domainNamesFile;
|
||||
|
||||
@Parameter(
|
||||
names = {"-g", "--gaining_registrar_id"},
|
||||
description = "The ID of the registrar to which domains should be transferred",
|
||||
required = true)
|
||||
private String gainingRegistrarId;
|
||||
|
||||
@Parameter(
|
||||
names = {"-l", "--losing_registrar_id"},
|
||||
description = "The ID of the registrar from which domains should be transferred",
|
||||
required = true)
|
||||
private String losingRegistrarId;
|
||||
|
||||
@Parameter(
|
||||
names = {"--reason"},
|
||||
description = "Reason to transfer the domains",
|
||||
required = true)
|
||||
private String reason;
|
||||
|
||||
@Parameter(
|
||||
names = {"--registrar_request"},
|
||||
description = "Whether the change was requested by a registrar.")
|
||||
private boolean requestedByRegistrar = false;
|
||||
|
||||
@Parameter(
|
||||
names = {"--max_qps"},
|
||||
description =
|
||||
"Maximum queries to run per second, otherwise the default (maxQps) will be used")
|
||||
private int maxQps;
|
||||
|
||||
private ServiceConnection connection;
|
||||
|
||||
@Override
|
||||
public void setConnection(ServiceConnection connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String prompt() throws Exception {
|
||||
checkArgument(
|
||||
domainNamesFile != null ^ (domains != null && !domains.isEmpty()),
|
||||
"Must specify exactly one input method, either --domains or --domain_names_file");
|
||||
return String.format("Attempt to transfer %d domains?", getDomainList().size());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String execute() throws Exception {
|
||||
checkArgument(
|
||||
Registrar.loadByRegistrarIdCached(gainingRegistrarId).isPresent(),
|
||||
"Gaining registrar %s doesn't exist",
|
||||
gainingRegistrarId);
|
||||
checkArgument(
|
||||
Registrar.loadByRegistrarIdCached(losingRegistrarId).isPresent(),
|
||||
"Losing registrar %s doesn't exist",
|
||||
losingRegistrarId);
|
||||
|
||||
ImmutableMap.Builder<String, Object> paramsBuilder = new ImmutableMap.Builder<>();
|
||||
paramsBuilder.put("gainingRegistrarId", gainingRegistrarId);
|
||||
paramsBuilder.put("losingRegistrarId", losingRegistrarId);
|
||||
paramsBuilder.put("requestedByRegistrar", requestedByRegistrar);
|
||||
paramsBuilder.put("reason", reason);
|
||||
if (maxQps > 0) {
|
||||
paramsBuilder.put("maxQps", maxQps);
|
||||
}
|
||||
ImmutableMap<String, Object> params = paramsBuilder.build();
|
||||
|
||||
for (List<String> batch : Iterables.partition(getDomainList(), DOMAIN_TRANSFER_BATCH_SIZE)) {
|
||||
System.out.printf("Sending batch of %d domains\n", batch.size());
|
||||
byte[] domainsList = GSON.toJson(batch).getBytes(UTF_8);
|
||||
System.out.println(
|
||||
connection.sendPostRequest(
|
||||
BulkDomainTransferAction.PATH, params, MediaType.PLAIN_TEXT_UTF_8, domainsList));
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private ImmutableList<String> getDomainList() throws IOException {
|
||||
return domainNamesFile == null ? ImmutableList.copyOf(domains) : loadDomainsFromFile();
|
||||
}
|
||||
|
||||
private ImmutableList<String> loadDomainsFromFile() throws IOException {
|
||||
return Splitter.on('\n')
|
||||
.omitEmptyStrings()
|
||||
.trimResults()
|
||||
.splitToStream(Files.asCharSource(new File(domainNamesFile), UTF_8).read())
|
||||
.map(DomainNameUtils::canonicalizeHostname)
|
||||
.collect(toImmutableList());
|
||||
}
|
||||
}
|
||||
@@ -16,17 +16,12 @@ package google.registry.tools;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Strings.isNullOrEmpty;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.pricing.PricingEngineProxy.getPricesForDomainName;
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
|
||||
import com.beust.jcommander.Parameter;
|
||||
import com.beust.jcommander.Parameters;
|
||||
import com.google.template.soy.data.SoyMapData;
|
||||
import google.registry.model.common.FeatureFlag;
|
||||
import google.registry.model.pricing.PremiumPricingEngine.DomainPrices;
|
||||
import google.registry.tools.soy.DomainCreateSoyInfo;
|
||||
import google.registry.util.StringGenerator;
|
||||
@@ -62,15 +57,6 @@ final class CreateDomainCommand extends CreateOrUpdateDomainCommand {
|
||||
|
||||
@Override
|
||||
protected void initMutatingEppToolCommand() {
|
||||
tm().transact(
|
||||
() -> {
|
||||
if (!FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_OPTIONAL)
|
||||
&& !FeatureFlag.isActiveNow(MINIMUM_DATASET_CONTACTS_PROHIBITED)) {
|
||||
checkArgumentNotNull(registrant, "Registrant must be specified");
|
||||
checkArgument(!admins.isEmpty(), "At least one admin must be specified");
|
||||
checkArgument(!techs.isEmpty(), "At least one tech must be specified");
|
||||
}
|
||||
});
|
||||
if (isNullOrEmpty(password)) {
|
||||
password = passwordGenerator.createString(PASSWORD_LENGTH);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.tools;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import com.beust.jcommander.Parameter;
|
||||
import com.beust.jcommander.Parameters;
|
||||
import google.registry.model.common.FeatureFlag;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Command to remove a {@link FeatureFlag} from the database entirely.
|
||||
*
|
||||
* <p>This should be used when a flag has been deprecated entirely, and we want to remove it, to
|
||||
* avoid having old invalid data in the database.
|
||||
*
|
||||
* <p>This command uses the native query format so that it is able to delete values that are no
|
||||
* longer part of the {@link FeatureFlag} enum.
|
||||
*
|
||||
* <p>This uses {@link ConfirmingCommand} instead of {@link MutatingCommand} because of the
|
||||
* nonstandard deletion flow required by the fact that the enum constant may already have been
|
||||
* removed.
|
||||
*/
|
||||
@Parameters(separators = " =", commandDescription = "Delete a FeatureFlag from the database")
|
||||
public class DeleteFeatureFlagCommand extends ConfirmingCommand {
|
||||
|
||||
@Parameter(description = "Feature flag to delete", required = true)
|
||||
private List<String> mainParameters;
|
||||
|
||||
@Override
|
||||
protected boolean checkExecutionState() {
|
||||
checkArgument(
|
||||
mainParameters != null && !mainParameters.isEmpty() && !mainParameters.getFirst().isBlank(),
|
||||
"Must provide a non-blank feature flag as the main parameter");
|
||||
boolean exists =
|
||||
tm().transact(
|
||||
() ->
|
||||
(long)
|
||||
tm().getEntityManager()
|
||||
.createNativeQuery(
|
||||
"SELECT COUNT(*) FROM \"FeatureFlag\" WHERE feature_name ="
|
||||
+ " :featureName",
|
||||
long.class)
|
||||
.setParameter("featureName", mainParameters.getFirst())
|
||||
.getSingleResult()
|
||||
> 0);
|
||||
if (!exists) {
|
||||
System.out.printf("No flag found with name '%s'", mainParameters.getFirst());
|
||||
}
|
||||
return exists;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String prompt() throws Exception {
|
||||
return String.format("Delete feature flag named '%s'?", mainParameters.getFirst());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String execute() throws Exception {
|
||||
String featureName = mainParameters.getFirst();
|
||||
tm().transact(
|
||||
() ->
|
||||
tm().getEntityManager()
|
||||
.createNativeQuery(
|
||||
"DELETE FROM \"FeatureFlag\" WHERE feature_name = :featureName")
|
||||
.setParameter("featureName", featureName)
|
||||
.executeUpdate());
|
||||
return String.format("Deleted feature flag with name '%s'", featureName);
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ public final class RegistryTool {
|
||||
public static final ImmutableMap<String, Class<? extends Command>> COMMAND_MAP =
|
||||
new ImmutableMap.Builder<String, Class<? extends Command>>()
|
||||
.put("ack_poll_messages", AckPollMessagesCommand.class)
|
||||
.put("bulk_domain_transfer", BulkDomainTransferCommand.class)
|
||||
.put("canonicalize_labels", CanonicalizeLabelsCommand.class)
|
||||
.put("check_domain", CheckDomainCommand.class)
|
||||
.put("check_domain_claims", CheckDomainClaimsCommand.class)
|
||||
@@ -54,6 +55,7 @@ public final class RegistryTool {
|
||||
.put("curl", CurlCommand.class)
|
||||
.put("delete_allocation_tokens", DeleteAllocationTokensCommand.class)
|
||||
.put("delete_domain", DeleteDomainCommand.class)
|
||||
.put("delete_feature_flag", DeleteFeatureFlagCommand.class)
|
||||
.put("delete_host", DeleteHostCommand.class)
|
||||
.put("delete_premium_list", DeletePremiumListCommand.class)
|
||||
.put("delete_reserved_list", DeleteReservedListCommand.class)
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.batch;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.DatabaseHelper.createTld;
|
||||
import static google.registry.testing.DatabaseHelper.loadByEntity;
|
||||
import static google.registry.testing.DatabaseHelper.persistDeletedDomain;
|
||||
import static google.registry.testing.DatabaseHelper.persistDomainWithDependentResources;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.util.concurrent.RateLimiter;
|
||||
import google.registry.flows.DaggerEppTestComponent;
|
||||
import google.registry.flows.EppController;
|
||||
import google.registry.flows.EppTestComponent.FakesAndMocksModule;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.eppcommon.StatusValue;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.FakeLockHandler;
|
||||
import google.registry.testing.FakeResponse;
|
||||
import google.registry.util.DateTimeUtils;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
||||
/** Tests for {@link BulkDomainTransferAction}. */
|
||||
public class BulkDomainTransferActionTest {
|
||||
|
||||
private final FakeClock fakeClock = new FakeClock(DateTime.parse("2024-01-01T00:00:00.000Z"));
|
||||
|
||||
@RegisterExtension
|
||||
final JpaIntegrationTestExtension jpa =
|
||||
new JpaTestExtensions.Builder().withClock(fakeClock).buildIntegrationTestExtension();
|
||||
|
||||
private final FakeResponse response = new FakeResponse();
|
||||
private final RateLimiter rateLimiter = mock(RateLimiter.class);
|
||||
|
||||
private Domain activeDomain;
|
||||
private Domain alreadyTransferredDomain;
|
||||
private Domain pendingDeleteDomain;
|
||||
private Domain deletedDomain;
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() throws Exception {
|
||||
createTld("tld");
|
||||
DateTime now = fakeClock.nowUtc();
|
||||
// The default registrar is TheRegistrar, which will be the losing registrar
|
||||
activeDomain =
|
||||
persistDomainWithDependentResources(
|
||||
"active", "tld", null, now, now.minusDays(1), DateTimeUtils.END_OF_TIME);
|
||||
alreadyTransferredDomain =
|
||||
persistResource(
|
||||
persistDomainWithDependentResources(
|
||||
"alreadytransferred",
|
||||
"tld",
|
||||
null,
|
||||
now,
|
||||
now.minusDays(1),
|
||||
DateTimeUtils.END_OF_TIME)
|
||||
.asBuilder()
|
||||
.setPersistedCurrentSponsorRegistrarId("NewRegistrar")
|
||||
.build());
|
||||
pendingDeleteDomain =
|
||||
persistResource(
|
||||
persistDomainWithDependentResources(
|
||||
"pendingdelete", "tld", null, now, now.minusDays(1), now.plusMonths(1))
|
||||
.asBuilder()
|
||||
.setStatusValues(ImmutableSet.of(StatusValue.PENDING_DELETE))
|
||||
.build());
|
||||
deletedDomain = persistDeletedDomain("deleted.tld", now.minusMonths(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_normalRun() {
|
||||
assertThat(activeDomain.getCurrentSponsorRegistrarId()).isEqualTo("TheRegistrar");
|
||||
assertThat(alreadyTransferredDomain.getCurrentSponsorRegistrarId()).isEqualTo("NewRegistrar");
|
||||
assertThat(pendingDeleteDomain.getCurrentSponsorRegistrarId()).isEqualTo("TheRegistrar");
|
||||
assertThat(deletedDomain.getCurrentSponsorRegistrarId()).isEqualTo("TheRegistrar");
|
||||
DateTime preRunTime = fakeClock.nowUtc();
|
||||
|
||||
BulkDomainTransferAction action =
|
||||
createAction("active.tld", "alreadytransferred.tld", "pendingdelete.tld", "deleted.tld");
|
||||
fakeClock.advanceOneMilli();
|
||||
|
||||
DateTime runTime = fakeClock.nowUtc();
|
||||
action.run();
|
||||
|
||||
fakeClock.advanceOneMilli();
|
||||
DateTime now = fakeClock.nowUtc();
|
||||
|
||||
// The active domain should have a new update timestamp and current registrar
|
||||
// The cloneProjectedAtTime calls are necessary to resolve the transfers, even though the
|
||||
// transfers have a time period of 0
|
||||
activeDomain = loadByEntity(activeDomain);
|
||||
assertThat(activeDomain.cloneProjectedAtTime(now).getCurrentSponsorRegistrarId())
|
||||
.isEqualTo("NewRegistrar");
|
||||
assertThat(activeDomain.getUpdateTimestamp().getTimestamp()).isEqualTo(runTime);
|
||||
|
||||
// The other three domains shouldn't change
|
||||
alreadyTransferredDomain = loadByEntity(alreadyTransferredDomain);
|
||||
assertThat(alreadyTransferredDomain.cloneProjectedAtTime(now).getCurrentSponsorRegistrarId())
|
||||
.isEqualTo("NewRegistrar");
|
||||
assertThat(alreadyTransferredDomain.getUpdateTimestamp().getTimestamp()).isEqualTo(preRunTime);
|
||||
|
||||
pendingDeleteDomain = loadByEntity(pendingDeleteDomain);
|
||||
assertThat(pendingDeleteDomain.cloneProjectedAtTime(now).getCurrentSponsorRegistrarId())
|
||||
.isEqualTo("TheRegistrar");
|
||||
assertThat(pendingDeleteDomain.getUpdateTimestamp().getTimestamp()).isEqualTo(preRunTime);
|
||||
|
||||
deletedDomain = loadByEntity(deletedDomain);
|
||||
assertThat(deletedDomain.cloneProjectedAtTime(now).getCurrentSponsorRegistrarId())
|
||||
.isEqualTo("TheRegistrar");
|
||||
assertThat(deletedDomain.getUpdateTimestamp().getTimestamp()).isEqualTo(preRunTime);
|
||||
}
|
||||
|
||||
private BulkDomainTransferAction createAction(String... domains) {
|
||||
EppController eppController =
|
||||
DaggerEppTestComponent.builder()
|
||||
.fakesAndMocksModule(FakesAndMocksModule.create(new FakeClock()))
|
||||
.build()
|
||||
.startRequest()
|
||||
.eppController();
|
||||
return new BulkDomainTransferAction(
|
||||
eppController,
|
||||
new FakeLockHandler(true),
|
||||
rateLimiter,
|
||||
ImmutableList.copyOf(domains),
|
||||
"NewRegistrar",
|
||||
"TheRegistrar",
|
||||
true,
|
||||
"reason",
|
||||
response);
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,6 @@ package google.registry.flows;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.ForeignKeyUtils.loadResource;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureStatus.INACTIVE;
|
||||
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
|
||||
import static google.registry.model.eppoutput.Result.Code.SUCCESS_AND_CLOSE;
|
||||
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACTION_PENDING;
|
||||
@@ -42,7 +40,6 @@ import com.google.re2j.Matcher;
|
||||
import com.google.re2j.Pattern;
|
||||
import google.registry.model.billing.BillingBase.Reason;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.common.FeatureFlag;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.DomainHistory;
|
||||
import google.registry.model.reporting.HistoryEntry.Type;
|
||||
@@ -76,19 +73,12 @@ class EppLifecycleDomainTest extends EppTestCase {
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
persistResource(
|
||||
new FeatureFlag()
|
||||
.asBuilder()
|
||||
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
|
||||
.setStatusMap(ImmutableSortedMap.of(START_OF_TIME, INACTIVE))
|
||||
.build());
|
||||
createTlds("example", "tld");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDomainDeleteRestore() throws Exception {
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
createContacts(DateTime.parse("2000-06-01T00:00:00Z"));
|
||||
|
||||
// Create domain example.tld
|
||||
assertThatCommand(
|
||||
@@ -148,7 +138,6 @@ class EppLifecycleDomainTest extends EppTestCase {
|
||||
@Test
|
||||
void testDomainDeleteRestore_duringAutorenewGracePeriod() throws Exception {
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
createContacts(DateTime.parse("2000-06-01T00:00:00Z"));
|
||||
|
||||
// Create domain example.tld
|
||||
assertThatCommand(
|
||||
@@ -222,7 +211,6 @@ class EppLifecycleDomainTest extends EppTestCase {
|
||||
@Test
|
||||
void testDomainDeleteRestore_duringRenewalGracePeriod() throws Exception {
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
createContacts(DateTime.parse("2000-06-01T00:00:00Z"));
|
||||
|
||||
// Create domain example.tld
|
||||
assertThatCommand(
|
||||
@@ -304,7 +292,6 @@ class EppLifecycleDomainTest extends EppTestCase {
|
||||
@Test
|
||||
void testDomainDelete_duringAddAndRenewalGracePeriod_deletesImmediately() throws Exception {
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
createContacts(DateTime.parse("2000-06-01T00:00:00Z"));
|
||||
|
||||
DateTime createTime = DateTime.parse("2000-06-01T00:02:00Z");
|
||||
// Create domain example.tld
|
||||
@@ -396,7 +383,6 @@ class EppLifecycleDomainTest extends EppTestCase {
|
||||
@Test
|
||||
void testDomainDeletion_withinAddGracePeriod_deletesImmediately() throws Exception {
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
createContacts(DateTime.parse("2000-06-01T00:00:00Z"));
|
||||
|
||||
// Create domain example.tld
|
||||
DateTime createTime = DateTime.parse("2000-06-01T00:02:00Z");
|
||||
@@ -450,7 +436,6 @@ class EppLifecycleDomainTest extends EppTestCase {
|
||||
@Test
|
||||
void testDomainDeletion_outsideAddGracePeriod_showsRedemptionPeriod() throws Exception {
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
createContacts(DateTime.parse("2000-06-01T00:00:00Z"));
|
||||
|
||||
DateTime createTime = DateTime.parse("2000-06-01T00:02:00Z");
|
||||
// Create domain example.tld
|
||||
@@ -509,7 +494,6 @@ class EppLifecycleDomainTest extends EppTestCase {
|
||||
@Test
|
||||
void testEapDomainDeletion_withinAddGracePeriod_eapFeeIsNotRefunded() throws Exception {
|
||||
assertThatCommand("login_valid_fee_extension.xml").hasSuccessfulLogin();
|
||||
createContacts(DateTime.parse("2000-06-01T00:00:00Z"));
|
||||
|
||||
// Set the EAP schedule.
|
||||
persistResource(
|
||||
@@ -697,7 +681,7 @@ class EppLifecycleDomainTest extends EppTestCase {
|
||||
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
|
||||
createContactsAndHosts();
|
||||
createHosts();
|
||||
|
||||
assertThatCommand("domain_create_sunrise_encoded_mark.xml")
|
||||
.atTime(sunriseDate.minusDays(1))
|
||||
@@ -760,11 +744,11 @@ class EppLifecycleDomainTest extends EppTestCase {
|
||||
.hasResponse(
|
||||
"poll_response_autorenew.xml",
|
||||
ImmutableMap.of(
|
||||
"ID", "15-2002",
|
||||
"ID", "11-2002",
|
||||
"QDATE", "2002-06-01T00:04:00Z",
|
||||
"DOMAIN", "fakesite.example",
|
||||
"EXDATE", "2003-06-01T00:04:00Z"));
|
||||
assertThatCommand("poll_ack.xml", ImmutableMap.of("ID", "15-2002"))
|
||||
assertThatCommand("poll_ack.xml", ImmutableMap.of("ID", "11-2002"))
|
||||
.atTime("2002-07-01T00:02:00Z")
|
||||
.hasResponse("poll_ack_response_empty.xml");
|
||||
|
||||
@@ -778,13 +762,13 @@ class EppLifecycleDomainTest extends EppTestCase {
|
||||
.hasResponse(
|
||||
"poll_response_autorenew.xml",
|
||||
ImmutableMap.of(
|
||||
"ID", "15-2003", // Note -- Year is different from previous ID.
|
||||
"ID", "11-2003", // Note -- Year is different from previous ID.
|
||||
"QDATE", "2003-06-01T00:04:00Z",
|
||||
"DOMAIN", "fakesite.example",
|
||||
"EXDATE", "2004-06-01T00:04:00Z"));
|
||||
|
||||
// Ack the second poll message and verify that none remain.
|
||||
assertThatCommand("poll_ack.xml", ImmutableMap.of("ID", "15-2003"))
|
||||
assertThatCommand("poll_ack.xml", ImmutableMap.of("ID", "11-2003"))
|
||||
.atTime("2003-07-01T00:05:05Z")
|
||||
.hasResponse("poll_ack_response_empty.xml");
|
||||
assertThatCommand("poll.xml")
|
||||
@@ -814,7 +798,7 @@ class EppLifecycleDomainTest extends EppTestCase {
|
||||
|
||||
// As the losing registrar, read the request poll message, and then ack it.
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
String messageId = "24-2001";
|
||||
String messageId = "20-2001";
|
||||
assertThatCommand("poll.xml")
|
||||
.atTime("2001-01-01T00:01:00Z")
|
||||
.hasResponse("poll_response_domain_transfer_request.xml", ImmutableMap.of("ID", messageId));
|
||||
@@ -823,7 +807,7 @@ class EppLifecycleDomainTest extends EppTestCase {
|
||||
.hasResponse("poll_ack_response_empty.xml");
|
||||
|
||||
// Five days in the future, expect a server approval poll message to the loser, and ack it.
|
||||
messageId = "23-2001";
|
||||
messageId = "19-2001";
|
||||
assertThatCommand("poll.xml")
|
||||
.atTime("2001-01-06T00:01:00Z")
|
||||
.hasResponse(
|
||||
@@ -835,7 +819,7 @@ class EppLifecycleDomainTest extends EppTestCase {
|
||||
assertThatLogoutSucceeds();
|
||||
|
||||
// Also expect a server approval poll message to the winner, with the transfer request trid.
|
||||
messageId = "22-2001";
|
||||
messageId = "18-2001";
|
||||
assertThatLoginSucceeds("TheRegistrar", "password2");
|
||||
assertThatCommand("poll.xml")
|
||||
.atTime("2001-01-06T00:02:00Z")
|
||||
@@ -1113,7 +1097,6 @@ class EppLifecycleDomainTest extends EppTestCase {
|
||||
createTlds("bar.foo.tld", "foo.tld");
|
||||
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
createContacts(DateTime.parse("2000-06-01T00:00:00.000Z"));
|
||||
|
||||
// Create domain example.bar.foo.tld
|
||||
assertThatCommand(
|
||||
@@ -1157,7 +1140,6 @@ class EppLifecycleDomainTest extends EppTestCase {
|
||||
createTld("tld.foo");
|
||||
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
createContacts(DateTime.parse("2000-06-01T00:00:00.000Z"));
|
||||
|
||||
// Create domain example.tld.foo
|
||||
assertThatCommand(
|
||||
@@ -1207,7 +1189,7 @@ class EppLifecycleDomainTest extends EppTestCase {
|
||||
.atTime(sunriseDate.minusDays(3))
|
||||
.hasSuccessfulLogin();
|
||||
|
||||
createContactsAndHosts();
|
||||
createHosts();
|
||||
|
||||
// During pre-delegation, any create should fail both with and without mark
|
||||
assertThatCommand("domain_create_sunrise_encoded_mark.xml", ImmutableMap.of("SMD", ENCODED_SMD))
|
||||
@@ -1235,9 +1217,10 @@ class EppLifecycleDomainTest extends EppTestCase {
|
||||
.hasResponse(
|
||||
"response_error.xml",
|
||||
ImmutableMap.of(
|
||||
"CODE", "2306",
|
||||
"CODE",
|
||||
"2306",
|
||||
"MSG",
|
||||
"Declared launch extension phase does not match the current registry phase"));
|
||||
"Declared launch extension phase does not match the current registry phase"));
|
||||
|
||||
// During sunrise, create with mark will succeed but without will fail.
|
||||
// We also test we can delete without a mark.
|
||||
@@ -1255,8 +1238,7 @@ class EppLifecycleDomainTest extends EppTestCase {
|
||||
.hasResponse("generic_success_response.xml");
|
||||
|
||||
assertThatCommand(
|
||||
"domain_create_no_hosts_or_dsdata.xml",
|
||||
ImmutableMap.of("DOMAIN", "general.example"))
|
||||
"domain_create_no_hosts_or_dsdata.xml", ImmutableMap.of("DOMAIN", "general.example"))
|
||||
.atTime(sunriseDate.plusDays(2))
|
||||
.hasResponse(
|
||||
"response_error.xml",
|
||||
@@ -1270,9 +1252,10 @@ class EppLifecycleDomainTest extends EppTestCase {
|
||||
.hasResponse(
|
||||
"response_error.xml",
|
||||
ImmutableMap.of(
|
||||
"CODE", "2306",
|
||||
"CODE",
|
||||
"2306",
|
||||
"MSG",
|
||||
"Declared launch extension phase does not match the current registry phase"));
|
||||
"Declared launch extension phase does not match the current registry phase"));
|
||||
|
||||
assertThatCommand(
|
||||
"domain_create_no_hosts_or_dsdata.xml", ImmutableMap.of("DOMAIN", "general.example"))
|
||||
@@ -1304,7 +1287,7 @@ class EppLifecycleDomainTest extends EppTestCase {
|
||||
.atTime(sunriseDate.minusDays(3))
|
||||
.hasSuccessfulLogin();
|
||||
|
||||
createContactsAndHosts();
|
||||
createHosts();
|
||||
|
||||
// During start-date sunrise, create with mark will succeed but without will fail.
|
||||
// We also test we can delete without a mark.
|
||||
@@ -1459,7 +1442,6 @@ class EppLifecycleDomainTest extends EppTestCase {
|
||||
void testDomainUpdateBySuperuser_sendsPollMessage() throws Exception {
|
||||
setIsSuperuser(false);
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
createContacts(DateTime.parse("2000-06-01T00:00:00Z"));
|
||||
|
||||
// Create domain example.tld
|
||||
assertThatCommand(
|
||||
|
||||
@@ -16,19 +16,13 @@ package google.registry.flows;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.ForeignKeyUtils.loadResource;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureStatus.INACTIVE;
|
||||
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
|
||||
import static google.registry.testing.DatabaseHelper.createTld;
|
||||
import static google.registry.testing.DatabaseHelper.createTlds;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static google.registry.testing.EppMetricSubject.assertThat;
|
||||
import static google.registry.testing.HostSubject.assertAboutHosts;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import google.registry.model.common.FeatureFlag;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.host.Host;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||
@@ -94,12 +88,6 @@ class EppLifecycleHostTest extends EppTestCase {
|
||||
|
||||
@Test
|
||||
void testRenamingHostToExistingHost_fails() throws Exception {
|
||||
persistResource(
|
||||
new FeatureFlag()
|
||||
.asBuilder()
|
||||
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
|
||||
.setStatusMap(ImmutableSortedMap.of(START_OF_TIME, INACTIVE))
|
||||
.build());
|
||||
createTld("example");
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
// Create the fakesite domain.
|
||||
@@ -150,12 +138,6 @@ class EppLifecycleHostTest extends EppTestCase {
|
||||
|
||||
@Test
|
||||
void testSuccess_multipartTldsWithSharedSuffixes() throws Exception {
|
||||
persistResource(
|
||||
new FeatureFlag()
|
||||
.asBuilder()
|
||||
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
|
||||
.setStatusMap(ImmutableSortedMap.of(START_OF_TIME, INACTIVE))
|
||||
.build());
|
||||
createTlds("bar.foo.tld", "foo.tld", "tld");
|
||||
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
|
||||
@@ -17,24 +17,18 @@ package google.registry.flows;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.EppResourceUtils.loadAtPointInTime;
|
||||
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureStatus.INACTIVE;
|
||||
import static google.registry.testing.DatabaseHelper.createTld;
|
||||
import static google.registry.testing.DatabaseHelper.loadAllOf;
|
||||
import static google.registry.testing.DatabaseHelper.loadByEntity;
|
||||
import static google.registry.testing.DatabaseHelper.persistActiveContact;
|
||||
import static google.registry.testing.DatabaseHelper.persistActiveHost;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
import static org.joda.time.Duration.standardDays;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import com.google.common.collect.Iterables;
|
||||
import google.registry.flows.EppTestComponent.FakesAndMocksModule;
|
||||
import google.registry.model.common.FeatureFlag;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.monitoring.whitebox.EppMetric;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||
@@ -60,12 +54,6 @@ class EppPointInTimeTest {
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
persistResource(
|
||||
new FeatureFlag()
|
||||
.asBuilder()
|
||||
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
|
||||
.setStatusMap(ImmutableSortedMap.of(START_OF_TIME, INACTIVE))
|
||||
.build());
|
||||
createTld("tld");
|
||||
}
|
||||
|
||||
|
||||
@@ -224,9 +224,8 @@ public class EppTestCase {
|
||||
}
|
||||
|
||||
/** Create the two administrative contacts and two hosts. */
|
||||
void createContactsAndHosts() throws Exception {
|
||||
void createHosts() throws Exception {
|
||||
DateTime createTime = DateTime.parse("2000-06-01T00:00:00Z");
|
||||
createContacts(createTime);
|
||||
assertThatCommand("host_create.xml", ImmutableMap.of("HOSTNAME", "ns1.example.external"))
|
||||
.atTime(createTime.plusMinutes(2))
|
||||
.hasResponse(
|
||||
@@ -243,21 +242,9 @@ public class EppTestCase {
|
||||
"CRDATE", createTime.plusMinutes(3).toString()));
|
||||
}
|
||||
|
||||
protected void createContacts(DateTime createTime) throws Exception {
|
||||
assertThatCommand("contact_create_sh8013.xml")
|
||||
.atTime(createTime)
|
||||
.hasResponse(
|
||||
"contact_create_response_sh8013.xml", ImmutableMap.of("CRDATE", createTime.toString()));
|
||||
assertThatCommand("contact_create_jd1234.xml")
|
||||
.atTime(createTime.plusMinutes(1))
|
||||
.hasResponse(
|
||||
"contact_create_response_jd1234.xml",
|
||||
ImmutableMap.of("CRDATE", createTime.plusMinutes(1).toString()));
|
||||
}
|
||||
|
||||
/** Creates the domain fakesite.example with two nameservers on it. */
|
||||
void createFakesite() throws Exception {
|
||||
createContactsAndHosts();
|
||||
createHosts();
|
||||
assertThatCommand("domain_create_fakesite.xml")
|
||||
.atTime("2000-06-01T00:04:00Z")
|
||||
.hasResponse(
|
||||
|
||||
@@ -24,10 +24,6 @@ import static google.registry.model.billing.BillingBase.Flag.RESERVED;
|
||||
import static google.registry.model.billing.BillingBase.Flag.SUNRISE;
|
||||
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.NONPREMIUM;
|
||||
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.SPECIFIED;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureStatus.ACTIVE;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureStatus.INACTIVE;
|
||||
import static google.registry.model.domain.fee.Fee.FEE_EXTENSION_URIS;
|
||||
import static google.registry.model.domain.token.AllocationToken.TokenType.BULK_PRICING;
|
||||
import static google.registry.model.domain.token.AllocationToken.TokenType.DEFAULT_PROMO;
|
||||
@@ -53,7 +49,6 @@ import static google.registry.testing.DatabaseHelper.deleteTld;
|
||||
import static google.registry.testing.DatabaseHelper.getHistoryEntries;
|
||||
import static google.registry.testing.DatabaseHelper.loadAllOf;
|
||||
import static google.registry.testing.DatabaseHelper.loadRegistrar;
|
||||
import static google.registry.testing.DatabaseHelper.newContact;
|
||||
import static google.registry.testing.DatabaseHelper.newHost;
|
||||
import static google.registry.testing.DatabaseHelper.persistActiveContact;
|
||||
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
|
||||
@@ -104,7 +99,6 @@ import google.registry.flows.domain.DomainFlowUtils.DomainLabelBlockedByBsaExcep
|
||||
import google.registry.flows.domain.DomainFlowUtils.DomainLabelTooLongException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.DomainNameExistsAsTldException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.DomainReservedException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.DuplicateContactForRoleException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.EmptyDomainNamePartException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.ExceedsMaxRegistrationYearsException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.ExpiredClaimException;
|
||||
@@ -123,12 +117,9 @@ import google.registry.flows.domain.DomainFlowUtils.LinkedResourceInPendingDelet
|
||||
import google.registry.flows.domain.DomainFlowUtils.LinkedResourcesDoNotExistException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.MalformedTcnIdException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.MaxSigLifeNotSupportedException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.MissingAdminContactException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.MissingBillingAccountMapException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.MissingClaimsNoticeException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.MissingContactTypeException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.MissingRegistrantException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.MissingTechnicalContactException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.NameserversNotAllowedForTldException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.NameserversNotSpecifiedForTldWithNameserverAllowListException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException;
|
||||
@@ -155,7 +146,6 @@ import google.registry.model.billing.BillingBase.Reason;
|
||||
import google.registry.model.billing.BillingBase.RenewalPriceBehavior;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.billing.BillingRecurrence;
|
||||
import google.registry.model.common.FeatureFlag;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.DomainHistory;
|
||||
import google.registry.model.domain.GracePeriod;
|
||||
@@ -1945,28 +1935,6 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
|
||||
assertThat(thrown).hasMessageThat().contains("ns2.example.net");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_missingContact() {
|
||||
persistActiveHost("ns1.example.net");
|
||||
persistActiveHost("ns2.example.net");
|
||||
persistActiveContact("jd1234");
|
||||
LinkedResourcesDoNotExistException thrown =
|
||||
assertThrows(LinkedResourcesDoNotExistException.class, this::runFlow);
|
||||
assertThat(thrown).hasMessageThat().contains("(sh8013)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_pendingDeleteContact() {
|
||||
persistActiveHost("ns1.example.net");
|
||||
persistActiveHost("ns2.example.net");
|
||||
persistActiveContact("sh8013");
|
||||
persistResource(newContact("jd1234").asBuilder().addStatusValue(PENDING_DELETE).build());
|
||||
clock.advanceOneMilli();
|
||||
LinkedResourceInPendingDeleteProhibitsOperationException thrown =
|
||||
assertThrows(LinkedResourceInPendingDeleteProhibitsOperationException.class, this::runFlow);
|
||||
assertThat(thrown).hasMessageThat().contains("jd1234");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_wrongTld() {
|
||||
persistContactsAndHosts("net");
|
||||
@@ -2072,14 +2040,6 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_duplicateContact() {
|
||||
setEppInput("domain_create_duplicate_contact.xml");
|
||||
persistContactsAndHosts();
|
||||
EppException thrown = assertThrows(DuplicateContactForRoleException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_missingContactType() {
|
||||
// We need to test for missing type, but not for invalid - the schema enforces that for us.
|
||||
@@ -2090,149 +2050,21 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_missingRegistrant() {
|
||||
setEppInput("domain_create_missing_registrant.xml");
|
||||
persistContactsAndHosts();
|
||||
EppException thrown = assertThrows(MissingRegistrantException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_minimumDatasetPhase1_missingRegistrant() throws Exception {
|
||||
persistResource(
|
||||
new FeatureFlag.Builder()
|
||||
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
|
||||
.setStatusMap(
|
||||
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
|
||||
.build());
|
||||
setEppInput("domain_create_missing_registrant.xml");
|
||||
persistContactsAndHosts();
|
||||
runFlowAssertResponse(
|
||||
loadFile("domain_create_response.xml", ImmutableMap.of("DOMAIN", "example.tld")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_minimumDatasetPhase2_noRegistrantButSomeOtherContactTypes() throws Exception {
|
||||
persistResource(
|
||||
new FeatureFlag.Builder()
|
||||
.setFeatureName(MINIMUM_DATASET_CONTACTS_PROHIBITED)
|
||||
.setStatusMap(
|
||||
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
|
||||
.build());
|
||||
setEppInput("domain_create_missing_registrant.xml");
|
||||
void testFailure_minimumDataset_noRegistrantButSomeOtherContactTypes() throws Exception {
|
||||
setEppInput("domain_create_other_contact_types.xml");
|
||||
persistContactsAndHosts();
|
||||
EppException thrown = assertThrows(ContactsProhibitedException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_missingAdmin() {
|
||||
setEppInput("domain_create_missing_admin.xml");
|
||||
persistContactsAndHosts();
|
||||
EppException thrown = assertThrows(MissingAdminContactException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_minimumDatasetPhase1_missingAdmin() throws Exception {
|
||||
persistResource(
|
||||
new FeatureFlag.Builder()
|
||||
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
|
||||
.setStatusMap(
|
||||
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
|
||||
.build());
|
||||
setEppInput("domain_create_missing_admin.xml");
|
||||
persistContactsAndHosts();
|
||||
runFlowAssertResponse(
|
||||
loadFile("domain_create_response.xml", ImmutableMap.of("DOMAIN", "example.tld")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_minimumDatasetPhase2_registrantAndOtherContactsSent() throws Exception {
|
||||
persistResource(
|
||||
new FeatureFlag.Builder()
|
||||
.setFeatureName(MINIMUM_DATASET_CONTACTS_PROHIBITED)
|
||||
.setStatusMap(
|
||||
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
|
||||
.build());
|
||||
setEppInput("domain_create_missing_admin.xml");
|
||||
void testFailure_minimumDataset_registrantNotPermitted() throws Exception {
|
||||
setEppInput("domain_create_has_registrant_contact.xml");
|
||||
persistContactsAndHosts();
|
||||
EppException thrown = assertThrows(RegistrantProhibitedException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_missingTech() {
|
||||
setEppInput("domain_create_missing_tech.xml");
|
||||
persistContactsAndHosts();
|
||||
EppException thrown = assertThrows(MissingTechnicalContactException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_minimumDatasetPhase1_missingTech() throws Exception {
|
||||
persistResource(
|
||||
new FeatureFlag.Builder()
|
||||
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
|
||||
.setStatusMap(
|
||||
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
|
||||
.build());
|
||||
setEppInput("domain_create_missing_tech.xml");
|
||||
persistContactsAndHosts();
|
||||
runFlowAssertResponse(
|
||||
loadFile("domain_create_response.xml", ImmutableMap.of("DOMAIN", "example.tld")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_missingNonRegistrantContacts() {
|
||||
setEppInput("domain_create_missing_non_registrant_contacts.xml");
|
||||
persistContactsAndHosts();
|
||||
EppException thrown = assertThrows(MissingAdminContactException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_minimumDatasetPhase1_missingNonRegistrantContacts() throws Exception {
|
||||
persistResource(
|
||||
new FeatureFlag.Builder()
|
||||
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
|
||||
.setStatusMap(
|
||||
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
|
||||
.build());
|
||||
setEppInput("domain_create_missing_non_registrant_contacts.xml");
|
||||
persistContactsAndHosts();
|
||||
runFlowAssertResponse(
|
||||
loadFile("domain_create_response.xml", ImmutableMap.of("DOMAIN", "example.tld")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_minimumDatasetPhase2_registrantNotPermitted() throws Exception {
|
||||
persistResource(
|
||||
new FeatureFlag.Builder()
|
||||
.setFeatureName(MINIMUM_DATASET_CONTACTS_PROHIBITED)
|
||||
.setStatusMap(
|
||||
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
|
||||
.build());
|
||||
setEppInput("domain_create_missing_non_registrant_contacts.xml");
|
||||
persistContactsAndHosts();
|
||||
EppException thrown = assertThrows(RegistrantProhibitedException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_minimumDatasetPhase2_noContactsWhatsoever() throws Exception {
|
||||
persistResource(
|
||||
new FeatureFlag.Builder()
|
||||
.setFeatureName(MINIMUM_DATASET_CONTACTS_PROHIBITED)
|
||||
.setStatusMap(
|
||||
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
|
||||
.build());
|
||||
setEppInput("domain_create_no_contacts.xml");
|
||||
persistContactsAndHosts();
|
||||
runFlowAssertResponse(
|
||||
loadFile("domain_create_response.xml", ImmutableMap.of("DOMAIN", "example.tld")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_badIdn() {
|
||||
createTld("xn--q9jyb4c");
|
||||
|
||||
@@ -23,7 +23,6 @@ import static google.registry.model.eppcommon.EppXmlTransformer.marshal;
|
||||
import static google.registry.model.tld.Tld.TldState.QUIET_PERIOD;
|
||||
import static google.registry.testing.DatabaseHelper.assertNoBillingEvents;
|
||||
import static google.registry.testing.DatabaseHelper.createTld;
|
||||
import static google.registry.testing.DatabaseHelper.persistActiveContact;
|
||||
import static google.registry.testing.DatabaseHelper.persistActiveHost;
|
||||
import static google.registry.testing.DatabaseHelper.persistBillingRecurrenceForDomain;
|
||||
import static google.registry.testing.DatabaseHelper.persistPremiumList;
|
||||
@@ -55,10 +54,6 @@ import google.registry.model.billing.BillingBase.Flag;
|
||||
import google.registry.model.billing.BillingBase.Reason;
|
||||
import google.registry.model.billing.BillingBase.RenewalPriceBehavior;
|
||||
import google.registry.model.billing.BillingRecurrence;
|
||||
import google.registry.model.contact.Contact;
|
||||
import google.registry.model.contact.ContactAuthInfo;
|
||||
import google.registry.model.domain.DesignatedContact;
|
||||
import google.registry.model.domain.DesignatedContact.Type;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.DomainAuthInfo;
|
||||
import google.registry.model.domain.DomainHistory;
|
||||
@@ -103,8 +98,6 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
|
||||
|
||||
private static final Pattern OK_PATTERN = Pattern.compile("\"ok\"");
|
||||
|
||||
private Contact registrant;
|
||||
private Contact contact;
|
||||
private Host host1;
|
||||
private Host host2;
|
||||
private Host host3;
|
||||
@@ -124,8 +117,6 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
|
||||
}
|
||||
|
||||
private void persistTestEntities(String domainName, boolean inactive) {
|
||||
registrant = persistActiveContact("jd1234");
|
||||
contact = persistActiveContact("sh8013");
|
||||
host1 = persistActiveHost("ns1.example.tld");
|
||||
host2 = persistActiveHost("ns1.example.net");
|
||||
domain =
|
||||
@@ -140,11 +131,6 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
|
||||
.setLastEppUpdateTime(DateTime.parse("1999-12-03T09:00:00.0Z"))
|
||||
.setLastTransferTime(DateTime.parse("2000-04-08T09:00:00.0Z"))
|
||||
.setRegistrationExpirationTime(DateTime.parse("2005-04-03T22:00:00.0Z"))
|
||||
.setRegistrant(Optional.of(registrant.createVKey()))
|
||||
.setContacts(
|
||||
ImmutableSet.of(
|
||||
DesignatedContact.create(Type.ADMIN, contact.createVKey()),
|
||||
DesignatedContact.create(Type.TECH, contact.createVKey())))
|
||||
.setNameservers(
|
||||
inactive ? null : ImmutableSet.of(host1.createVKey(), host2.createVKey()))
|
||||
.setAuthInfo(DomainAuthInfo.create(PasswordAuth.create("2fooBAR")))
|
||||
@@ -323,24 +309,6 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
|
||||
doSuccessfulTest("domain_info_response.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_differentRegistrarWithRegistrantAuthInfo() throws Exception {
|
||||
persistTestEntities(false);
|
||||
setEppInput("domain_info_with_contact_auth.xml");
|
||||
eppLoader.replaceAll("JD1234-REP", registrant.getRepoId());
|
||||
sessionMetadata.setRegistrarId("ClientZ");
|
||||
doSuccessfulTest("domain_info_response.xml", false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_differentRegistrarWithContactAuthInfo() throws Exception {
|
||||
persistTestEntities(false);
|
||||
setEppInput("domain_info_with_contact_auth.xml");
|
||||
eppLoader.replaceAll("JD1234-REP", registrant.getRepoId());
|
||||
sessionMetadata.setRegistrarId("ClientZ");
|
||||
doSuccessfulTest("domain_info_response.xml", false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_inQuietPeriod() throws Exception {
|
||||
persistResource(
|
||||
@@ -618,99 +586,6 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_differentRegistrarWrongRegistrantAuthInfo() {
|
||||
persistTestEntities(false);
|
||||
// Change the password of the registrant so that it does not match the file.
|
||||
registrant =
|
||||
persistResource(
|
||||
registrant
|
||||
.asBuilder()
|
||||
.setAuthInfo(ContactAuthInfo.create(PasswordAuth.create("diffpw")))
|
||||
.build());
|
||||
sessionMetadata.setRegistrarId("ClientZ");
|
||||
setEppInput("domain_info_with_contact_auth.xml");
|
||||
// Replace the ROID in the xml file with the one for our registrant.
|
||||
eppLoader.replaceAll("JD1234-REP", registrant.getRepoId());
|
||||
EppException thrown = assertThrows(BadAuthInfoForResourceException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_wrongRegistrantAuthInfo() {
|
||||
persistTestEntities(false);
|
||||
// Change the password of the registrant so that it does not match the file.
|
||||
registrant =
|
||||
persistResource(
|
||||
registrant
|
||||
.asBuilder()
|
||||
.setAuthInfo(ContactAuthInfo.create(PasswordAuth.create("diffpw")))
|
||||
.build());
|
||||
setEppInput("domain_info_with_contact_auth.xml");
|
||||
// Replace the ROID in the xml file with the one for our registrant.
|
||||
eppLoader.replaceAll("JD1234-REP", registrant.getRepoId());
|
||||
EppException thrown = assertThrows(BadAuthInfoForResourceException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_differentRegistrarWrongContactAuthInfo() {
|
||||
persistTestEntities(false);
|
||||
// Change the password of the contact so that it does not match the file.
|
||||
contact =
|
||||
persistResource(
|
||||
contact
|
||||
.asBuilder()
|
||||
.setAuthInfo(ContactAuthInfo.create(PasswordAuth.create("diffpw")))
|
||||
.build());
|
||||
sessionMetadata.setRegistrarId("ClientZ");
|
||||
setEppInput("domain_info_with_contact_auth.xml");
|
||||
// Replace the ROID in the xml file with the one for our contact.
|
||||
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
|
||||
EppException thrown = assertThrows(BadAuthInfoForResourceException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_wrongContactAuthInfo() {
|
||||
persistTestEntities(false);
|
||||
// Change the password of the contact so that it does not match the file.
|
||||
contact =
|
||||
persistResource(
|
||||
contact
|
||||
.asBuilder()
|
||||
.setAuthInfo(ContactAuthInfo.create(PasswordAuth.create("diffpw")))
|
||||
.build());
|
||||
setEppInput("domain_info_with_contact_auth.xml");
|
||||
// Replace the ROID in the xml file with the one for our contact.
|
||||
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
|
||||
EppException thrown = assertThrows(BadAuthInfoForResourceException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_differentRegistrarUnrelatedContactAuthInfo() {
|
||||
persistTestEntities(false);
|
||||
Contact unrelatedContact = persistActiveContact("foo1234");
|
||||
sessionMetadata.setRegistrarId("ClientZ");
|
||||
setEppInput("domain_info_with_contact_auth.xml");
|
||||
// Replace the ROID in the xml file with the one for our unrelated contact.
|
||||
eppLoader.replaceAll("JD1234-REP", unrelatedContact.getRepoId());
|
||||
EppException thrown = assertThrows(BadAuthInfoForResourceException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_unrelatedContactAuthInfo() {
|
||||
persistTestEntities(false);
|
||||
Contact unrelatedContact = persistActiveContact("foo1234");
|
||||
setEppInput("domain_info_with_contact_auth.xml");
|
||||
// Replace the ROID in the xml file with the one for our unrelated contact.
|
||||
eppLoader.replaceAll("JD1234-REP", unrelatedContact.getRepoId());
|
||||
EppException thrown = assertThrows(BadAuthInfoForResourceException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test create command. Fee extension version 6 is the only one which supports fee extensions on
|
||||
* info commands and responses, so we don't need to test the other versions.
|
||||
@@ -719,10 +594,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
|
||||
void testFeeExtension_createCommand() throws Exception {
|
||||
setEppInput(
|
||||
"domain_info_fee.xml",
|
||||
updateSubstitutions(
|
||||
SUBSTITUTION_BASE,
|
||||
"COMMAND", "create",
|
||||
"PERIOD", "2"));
|
||||
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "create", "PERIOD", "2"));
|
||||
persistTestEntities(false);
|
||||
setUpBillingEventForExistingDomain();
|
||||
doSuccessfulTest(
|
||||
@@ -741,10 +613,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
|
||||
void testFeeExtension_renewCommand() throws Exception {
|
||||
setEppInput(
|
||||
"domain_info_fee.xml",
|
||||
updateSubstitutions(
|
||||
SUBSTITUTION_BASE,
|
||||
"COMMAND", "renew",
|
||||
"PERIOD", "2"));
|
||||
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "renew", "PERIOD", "2"));
|
||||
persistTestEntities(false);
|
||||
setUpBillingEventForExistingDomain();
|
||||
doSuccessfulTest(
|
||||
@@ -763,10 +632,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
|
||||
void testFeeExtension_transferCommand() throws Exception {
|
||||
setEppInput(
|
||||
"domain_info_fee.xml",
|
||||
updateSubstitutions(
|
||||
SUBSTITUTION_BASE,
|
||||
"COMMAND", "transfer",
|
||||
"PERIOD", "1"));
|
||||
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "transfer", "PERIOD", "1"));
|
||||
persistTestEntities(false);
|
||||
setUpBillingEventForExistingDomain();
|
||||
doSuccessfulTest(
|
||||
@@ -785,10 +651,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
|
||||
void testFeeExtension_restoreCommand() throws Exception {
|
||||
setEppInput(
|
||||
"domain_info_fee.xml",
|
||||
updateSubstitutions(
|
||||
SUBSTITUTION_BASE,
|
||||
"COMMAND", "restore",
|
||||
"PERIOD", "1"));
|
||||
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "restore", "PERIOD", "1"));
|
||||
persistTestEntities(false);
|
||||
setUpBillingEventForExistingDomain();
|
||||
doSuccessfulTest("domain_info_fee_restore_response.xml", false, ImmutableMap.of(), true);
|
||||
@@ -838,10 +701,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
|
||||
setEppInput(
|
||||
"domain_info_fee.xml",
|
||||
updateSubstitutions(
|
||||
SUBSTITUTION_BASE,
|
||||
"NAME", "rich.example",
|
||||
"COMMAND", "create",
|
||||
"PERIOD", "1"));
|
||||
SUBSTITUTION_BASE, "NAME", "rich.example", "COMMAND", "create", "PERIOD", "1"));
|
||||
persistTestEntities("rich.example", false);
|
||||
setUpBillingEventForExistingDomain();
|
||||
doSuccessfulTest(
|
||||
@@ -858,10 +718,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
|
||||
setEppInput(
|
||||
"domain_info_fee.xml",
|
||||
updateSubstitutions(
|
||||
SUBSTITUTION_BASE,
|
||||
"NAME", "rich.example",
|
||||
"COMMAND", "renew",
|
||||
"PERIOD", "1"));
|
||||
SUBSTITUTION_BASE, "NAME", "rich.example", "COMMAND", "renew", "PERIOD", "1"));
|
||||
persistTestEntities("rich.example", false);
|
||||
setUpBillingEventForExistingDomain();
|
||||
doSuccessfulTest(
|
||||
@@ -973,10 +830,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
|
||||
setEppInput(
|
||||
"domain_info_fee.xml",
|
||||
updateSubstitutions(
|
||||
SUBSTITUTION_BASE,
|
||||
"NAME", "rich.example",
|
||||
"COMMAND", "transfer",
|
||||
"PERIOD", "1"));
|
||||
SUBSTITUTION_BASE, "NAME", "rich.example", "COMMAND", "transfer", "PERIOD", "1"));
|
||||
persistTestEntities("rich.example", false);
|
||||
setUpBillingEventForExistingDomain();
|
||||
doSuccessfulTest(
|
||||
@@ -993,10 +847,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
|
||||
setEppInput(
|
||||
"domain_info_fee.xml",
|
||||
updateSubstitutions(
|
||||
SUBSTITUTION_BASE,
|
||||
"NAME", "rich.example",
|
||||
"COMMAND", "restore",
|
||||
"PERIOD", "1"));
|
||||
SUBSTITUTION_BASE, "NAME", "rich.example", "COMMAND", "restore", "PERIOD", "1"));
|
||||
persistTestEntities("rich.example", false);
|
||||
setUpBillingEventForExistingDomain();
|
||||
doSuccessfulTest(
|
||||
@@ -1009,10 +860,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
|
||||
setEppInput(
|
||||
"domain_info_fee.xml",
|
||||
updateSubstitutions(
|
||||
SUBSTITUTION_BASE,
|
||||
"COMMAND", "create",
|
||||
"CURRENCY", "EUR",
|
||||
"PERIOD", "1"));
|
||||
SUBSTITUTION_BASE, "COMMAND", "create", "CURRENCY", "EUR", "PERIOD", "1"));
|
||||
persistTestEntities(false);
|
||||
setUpBillingEventForExistingDomain();
|
||||
EppException thrown = assertThrows(CurrencyUnitMismatchException.class, this::runFlow);
|
||||
@@ -1024,10 +872,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
|
||||
setEppInput(
|
||||
"domain_info_fee.xml",
|
||||
updateSubstitutions(
|
||||
SUBSTITUTION_BASE,
|
||||
"COMMAND", "create",
|
||||
"CURRENCY", "BAD",
|
||||
"PERIOD", "1"));
|
||||
SUBSTITUTION_BASE, "COMMAND", "create", "CURRENCY", "BAD", "PERIOD", "1"));
|
||||
EppException thrown = assertThrows(UnknownCurrencyEppException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
@@ -1037,11 +882,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
|
||||
void testFeeExtension_periodNotInYears() {
|
||||
setEppInput(
|
||||
"domain_info_fee.xml",
|
||||
updateSubstitutions(
|
||||
SUBSTITUTION_BASE,
|
||||
"COMMAND", "create",
|
||||
"PERIOD", "2",
|
||||
"UNIT", "m"));
|
||||
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "create", "PERIOD", "2", "UNIT", "m"));
|
||||
persistTestEntities(false);
|
||||
setUpBillingEventForExistingDomain();
|
||||
EppException thrown = assertThrows(BadPeriodUnitException.class, this::runFlow);
|
||||
@@ -1073,10 +914,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
|
||||
void testFeeExtension_multiyearRestore() {
|
||||
setEppInput(
|
||||
"domain_info_fee.xml",
|
||||
updateSubstitutions(
|
||||
SUBSTITUTION_BASE,
|
||||
"COMMAND", "restore",
|
||||
"PERIOD", "2"));
|
||||
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "restore", "PERIOD", "2"));
|
||||
persistTestEntities(false);
|
||||
setUpBillingEventForExistingDomain();
|
||||
EppException thrown = assertThrows(RestoresAreAlwaysForOneYearException.class, this::runFlow);
|
||||
@@ -1088,10 +926,7 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
|
||||
void testFeeExtension_multiyearTransfer() {
|
||||
setEppInput(
|
||||
"domain_info_fee.xml",
|
||||
updateSubstitutions(
|
||||
SUBSTITUTION_BASE,
|
||||
"COMMAND", "transfer",
|
||||
"PERIOD", "2"));
|
||||
updateSubstitutions(SUBSTITUTION_BASE, "COMMAND", "transfer", "PERIOD", "2"));
|
||||
persistTestEntities(false);
|
||||
setUpBillingEventForExistingDomain();
|
||||
EppException thrown = assertThrows(TransfersAreAlwaysForOneYearException.class, this::runFlow);
|
||||
|
||||
@@ -1004,6 +1004,40 @@ class DomainTransferRequestFlowTest
|
||||
ImmutableMap.of("PERIOD", "0", "AUTOMATIC_TRANSFER_LENGTH", "5")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_superuserExtension_clientTransferProhibited() throws Exception {
|
||||
setupDomain("example", "tld");
|
||||
eppRequestSource = EppRequestSource.TOOL;
|
||||
domain =
|
||||
persistResource(
|
||||
domain.asBuilder().addStatusValue(StatusValue.CLIENT_TRANSFER_PROHIBITED).build());
|
||||
doSuccessfulSuperuserExtensionTest(
|
||||
"domain_transfer_request_superuser_extension.xml",
|
||||
"domain_transfer_request_response_su_ext_zero_period_zero_transfer_length.xml",
|
||||
domain.getRegistrationExpirationTime().plusYears(0),
|
||||
ImmutableMap.of("PERIOD", "0", "AUTOMATIC_TRANSFER_LENGTH", "0"),
|
||||
Optional.empty(),
|
||||
Period.create(0, Unit.YEARS),
|
||||
Duration.ZERO);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_superuserExtension_serverTransferProhibited() throws Exception {
|
||||
setupDomain("example", "tld");
|
||||
eppRequestSource = EppRequestSource.TOOL;
|
||||
domain =
|
||||
persistResource(
|
||||
domain.asBuilder().addStatusValue(StatusValue.SERVER_TRANSFER_PROHIBITED).build());
|
||||
doSuccessfulSuperuserExtensionTest(
|
||||
"domain_transfer_request_superuser_extension.xml",
|
||||
"domain_transfer_request_response_su_ext_zero_period_zero_transfer_length.xml",
|
||||
domain.getRegistrationExpirationTime().plusYears(0),
|
||||
ImmutableMap.of("PERIOD", "0", "AUTOMATIC_TRANSFER_LENGTH", "0"),
|
||||
Optional.empty(),
|
||||
Period.create(0, Unit.YEARS),
|
||||
Duration.ZERO);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_cappedExpiration() throws Exception {
|
||||
setupDomain("example", "tld");
|
||||
@@ -1809,6 +1843,22 @@ class DomainTransferRequestFlowTest
|
||||
assertThat(thrown).hasMessageThat().contains("pendingDelete");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_pendingDelete_evenWhenSuperuser() {
|
||||
setupDomain("example", "tld");
|
||||
eppRequestSource = EppRequestSource.TOOL;
|
||||
domain = persistResource(domain.asBuilder().addStatusValue(StatusValue.PENDING_DELETE).build());
|
||||
ResourceStatusProhibitsOperationException thrown =
|
||||
assertThrows(
|
||||
ResourceStatusProhibitsOperationException.class,
|
||||
() ->
|
||||
runTest(
|
||||
"domain_transfer_request_superuser_extension.xml",
|
||||
UserPrivileges.SUPERUSER,
|
||||
ImmutableMap.of("PERIOD", "0", "AUTOMATIC_TRANSFER_LENGTH", "0")));
|
||||
assertThat(thrown).hasMessageThat().contains("pendingDelete");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIcannActivityReportField_getsLogged() throws Exception {
|
||||
setupDomain("example", "tld");
|
||||
|
||||
@@ -19,10 +19,6 @@ import static com.google.common.collect.Sets.union;
|
||||
import static com.google.common.io.BaseEncoding.base16;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.ForeignKeyUtils.loadResource;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureStatus.ACTIVE;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureStatus.INACTIVE;
|
||||
import static google.registry.model.eppcommon.StatusValue.CLIENT_DELETE_PROHIBITED;
|
||||
import static google.registry.model.eppcommon.StatusValue.CLIENT_HOLD;
|
||||
import static google.registry.model.eppcommon.StatusValue.CLIENT_RENEW_PROHIBITED;
|
||||
@@ -45,7 +41,6 @@ import static google.registry.testing.DatabaseHelper.assertPollMessagesForResour
|
||||
import static google.registry.testing.DatabaseHelper.createTld;
|
||||
import static google.registry.testing.DatabaseHelper.getOnlyHistoryEntryOfType;
|
||||
import static google.registry.testing.DatabaseHelper.getPollMessages;
|
||||
import static google.registry.testing.DatabaseHelper.loadByKey;
|
||||
import static google.registry.testing.DatabaseHelper.loadRegistrar;
|
||||
import static google.registry.testing.DatabaseHelper.persistActiveContact;
|
||||
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
|
||||
@@ -68,13 +63,14 @@ import google.registry.config.RegistryConfig;
|
||||
import google.registry.flows.EppException;
|
||||
import google.registry.flows.EppException.UnimplementedExtensionException;
|
||||
import google.registry.flows.EppRequestSource;
|
||||
import google.registry.flows.FlowTestCase.CommitMode;
|
||||
import google.registry.flows.FlowTestCase.UserPrivileges;
|
||||
import google.registry.flows.FlowUtils.NotLoggedInException;
|
||||
import google.registry.flows.ResourceFlowTestCase;
|
||||
import google.registry.flows.ResourceFlowUtils.AddRemoveSameValueException;
|
||||
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
|
||||
import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException;
|
||||
import google.registry.flows.ResourceFlowUtils.StatusNotClientSettableException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.DuplicateContactForRoleException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.EmptySecDnsUpdateException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.FeesMismatchException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.FeesRequiredForNonFreeOperationException;
|
||||
@@ -82,10 +78,6 @@ import google.registry.flows.domain.DomainFlowUtils.InvalidDsRecordException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.LinkedResourceInPendingDeleteProhibitsOperationException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.LinkedResourcesDoNotExistException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.MaxSigLifeChangeNotSupportedException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.MissingAdminContactException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.MissingContactTypeException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.MissingRegistrantException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.MissingTechnicalContactException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.NameserversNotAllowedForTldException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.NameserversNotSpecifiedForTldWithNameserverAllowListException;
|
||||
import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException;
|
||||
@@ -101,7 +93,6 @@ import google.registry.flows.exceptions.ResourceStatusProhibitsOperationExceptio
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.billing.BillingBase.Reason;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.common.FeatureFlag;
|
||||
import google.registry.model.contact.Contact;
|
||||
import google.registry.model.domain.DesignatedContact;
|
||||
import google.registry.model.domain.DesignatedContact.Type;
|
||||
@@ -320,38 +311,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_emptyRegistrant() throws Exception {
|
||||
setEppInput("domain_update_empty_registrant.xml");
|
||||
persistReferencedEntities();
|
||||
persistDomain();
|
||||
MissingRegistrantException thrown =
|
||||
assertThrows(MissingRegistrantException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_minimumDatasetPhase1_emptyRegistrant() throws Exception {
|
||||
persistResource(
|
||||
new FeatureFlag.Builder()
|
||||
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
|
||||
.setStatusMap(
|
||||
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
|
||||
.build());
|
||||
setEppInput("domain_update_empty_registrant.xml");
|
||||
persistReferencedEntities();
|
||||
persistDomain();
|
||||
runFlowAssertResponse(loadFile("generic_success_response.xml"));
|
||||
assertThat(reloadResourceByForeignKey().getRegistrant()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_minimumDatasetPhase2_whenAddingNewContacts() throws Exception {
|
||||
persistResource(
|
||||
new FeatureFlag.Builder()
|
||||
.setFeatureName(MINIMUM_DATASET_CONTACTS_PROHIBITED)
|
||||
.setStatusMap(
|
||||
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
|
||||
.build());
|
||||
void testFailure_minimumDataset_whenAddingNewContacts() throws Exception {
|
||||
// This EPP adds a new technical contact mak21 that wasn't already present.
|
||||
setEppInput("domain_update_empty_registrant.xml");
|
||||
persistReferencedEntities();
|
||||
@@ -386,7 +346,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_addAndRemoveLargeNumberOfNameserversAndContacts() throws Exception {
|
||||
void testSuccess_addAndRemoveLargeNumberOfNameservers() throws Exception {
|
||||
persistReferencedEntities();
|
||||
persistDomain();
|
||||
setEppInput("domain_update_max_everything.xml");
|
||||
@@ -398,20 +358,10 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
|
||||
nameservers.add(host.createVKey());
|
||||
}
|
||||
}
|
||||
ImmutableList.Builder<DesignatedContact> contactsBuilder = new ImmutableList.Builder<>();
|
||||
for (int i = 0; i < 8; i++) {
|
||||
contactsBuilder.add(
|
||||
DesignatedContact.create(
|
||||
DesignatedContact.Type.values()[i % 4],
|
||||
persistActiveContact(String.format("max_test_%d", i)).createVKey()));
|
||||
}
|
||||
ImmutableList<DesignatedContact> contacts = contactsBuilder.build();
|
||||
persistResource(
|
||||
reloadResourceByForeignKey()
|
||||
.asBuilder()
|
||||
.setNameservers(nameservers.build())
|
||||
.setContacts(ImmutableSet.copyOf(contacts.subList(0, 3)))
|
||||
.setRegistrant(Optional.of(contacts.get(3).getContactKey()))
|
||||
.build());
|
||||
clock.advanceOneMilli();
|
||||
assertMutatingFlow(true);
|
||||
@@ -419,9 +369,6 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
|
||||
Domain domain = reloadResourceByForeignKey();
|
||||
assertAboutDomains().that(domain).hasOneHistoryEntryEachOfTypes(DOMAIN_CREATE, DOMAIN_UPDATE);
|
||||
assertThat(domain.getNameservers()).hasSize(13);
|
||||
// getContacts does not return contacts of type REGISTRANT, so check these separately.
|
||||
assertThat(domain.getContacts()).hasSize(3);
|
||||
assertThat(loadByKey(domain.getRegistrant().get()).getContactId()).isEqualTo("max_test_7");
|
||||
assertNoBillingEvents();
|
||||
assertDomainDnsRequests("example.tld");
|
||||
}
|
||||
@@ -503,26 +450,6 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
|
||||
runFlowAssertResponse(loadFile("generic_success_response.xml"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_multipleReferencesToSameContactRemoved() throws Exception {
|
||||
setEppInput("domain_update_remove_multiple_contacts.xml");
|
||||
persistReferencedEntities();
|
||||
Contact sh8013 = loadResource(Contact.class, "sh8013", clock.nowUtc()).get();
|
||||
VKey<Contact> sh8013Key = sh8013.createVKey();
|
||||
persistResource(
|
||||
DatabaseHelper.newDomain(getUniqueIdFromCommand())
|
||||
.asBuilder()
|
||||
.setRegistrant(Optional.of(sh8013Key))
|
||||
.setContacts(
|
||||
ImmutableSet.of(
|
||||
DesignatedContact.create(Type.ADMIN, sh8013Key),
|
||||
DesignatedContact.create(Type.BILLING, sh8013Key),
|
||||
DesignatedContact.create(Type.TECH, sh8013Key)))
|
||||
.build());
|
||||
clock.advanceOneMilli();
|
||||
runFlowAssertResponse(loadFile("generic_success_response.xml"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_removeClientUpdateProhibited() throws Exception {
|
||||
persistReferencedEntities();
|
||||
@@ -1180,40 +1107,6 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
|
||||
assertThat(thrown).hasMessageThat().contains("(ns2.example.foo)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_missingContact() throws Exception {
|
||||
persistActiveHost("ns1.example.foo");
|
||||
persistActiveHost("ns2.example.foo");
|
||||
persistActiveContact("mak21");
|
||||
persistActiveDomain(getUniqueIdFromCommand());
|
||||
LinkedResourcesDoNotExistException thrown =
|
||||
assertThrows(LinkedResourcesDoNotExistException.class, this::runFlow);
|
||||
assertThat(thrown).hasMessageThat().contains("(sh8013)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_addingDuplicateContact() throws Exception {
|
||||
persistReferencedEntities();
|
||||
persistActiveContact("foo");
|
||||
persistDomain();
|
||||
// Add a tech contact to the persisted entity, which should cause the flow to fail when it tries
|
||||
// to add "mak21" as a second tech contact.
|
||||
persistResource(
|
||||
reloadResourceByForeignKey()
|
||||
.asBuilder()
|
||||
.setContacts(
|
||||
DesignatedContact.create(
|
||||
Type.TECH,
|
||||
loadResource(Contact.class, "foo", clock.nowUtc()).get().createVKey()))
|
||||
.build());
|
||||
EppException thrown = assertThrows(DuplicateContactForRoleException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
assertThat(thrown.getResult().getMsg())
|
||||
.isEqualTo(
|
||||
"More than one contact for a given role is not allowed: "
|
||||
+ "role [tech] has contacts [foo, mak21]");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_statusValueNotClientSettable() throws Exception {
|
||||
setEppInput("domain_update_prohibited_status.xml");
|
||||
@@ -1425,40 +1318,6 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
|
||||
assertThat(thrown).hasMessageThat().contains("pendingDelete");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_duplicateContactInCommand() throws Exception {
|
||||
setEppInput("domain_update_duplicate_contact.xml");
|
||||
persistReferencedEntities();
|
||||
persistDomain();
|
||||
EppException thrown = assertThrows(DuplicateContactForRoleException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_multipleDuplicateContactInCommand() throws Exception {
|
||||
setEppInput("domain_update_multiple_duplicate_contacts.xml");
|
||||
persistReferencedEntities();
|
||||
persistDomain();
|
||||
EppException thrown = assertThrows(DuplicateContactForRoleException.class, this::runFlow);
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
"More than one contact for a given role is not allowed: "
|
||||
+ "role [billing] has contacts [mak21, sh8013], "
|
||||
+ "role [tech] has contacts [mak21, sh8013]");
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_missingContactType() throws Exception {
|
||||
// We need to test for missing type, but not for invalid - the schema enforces that for us.
|
||||
setEppInput("domain_update_missing_contact_type.xml");
|
||||
persistReferencedEntities();
|
||||
persistDomain();
|
||||
EppException thrown = assertThrows(MissingContactTypeException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_unauthorizedClient() throws Exception {
|
||||
sessionMetadata.setRegistrarId("NewRegistrar");
|
||||
@@ -1514,68 +1373,8 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
// Contacts mismatch.
|
||||
@Test
|
||||
void testFailure_sameContactAddedAndRemoved() throws Exception {
|
||||
setEppInput("domain_update_add_remove_same_contact.xml");
|
||||
persistReferencedEntities();
|
||||
persistResource(
|
||||
DatabaseHelper.newDomain(getUniqueIdFromCommand())
|
||||
.asBuilder()
|
||||
.setContacts(
|
||||
DesignatedContact.create(
|
||||
Type.TECH,
|
||||
loadResource(Contact.class, "sh8013", clock.nowUtc()).get().createVKey()))
|
||||
.build());
|
||||
EppException thrown = assertThrows(AddRemoveSameValueException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_removeAdmin() throws Exception {
|
||||
setEppInput("domain_update_remove_admin.xml");
|
||||
persistReferencedEntities();
|
||||
persistResource(
|
||||
DatabaseHelper.newDomain(getUniqueIdFromCommand())
|
||||
.asBuilder()
|
||||
.setContacts(
|
||||
ImmutableSet.of(
|
||||
DesignatedContact.create(Type.ADMIN, sh8013Contact.createVKey()),
|
||||
DesignatedContact.create(Type.TECH, sh8013Contact.createVKey())))
|
||||
.build());
|
||||
EppException thrown = assertThrows(MissingAdminContactException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_minimumDatasetPhase1_removeAdmin() throws Exception {
|
||||
persistResource(
|
||||
new FeatureFlag.Builder()
|
||||
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
|
||||
.setStatusMap(
|
||||
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
|
||||
.build());
|
||||
setEppInput("domain_update_remove_admin.xml");
|
||||
persistReferencedEntities();
|
||||
persistResource(
|
||||
DatabaseHelper.newDomain(getUniqueIdFromCommand())
|
||||
.asBuilder()
|
||||
.setContacts(
|
||||
ImmutableSet.of(
|
||||
DesignatedContact.create(Type.ADMIN, sh8013Contact.createVKey()),
|
||||
DesignatedContact.create(Type.TECH, sh8013Contact.createVKey())))
|
||||
.build());
|
||||
runFlowAssertResponse(loadFile("generic_success_response.xml"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_minimumDatasetPhase2_addingNewRegistrantFails() throws Exception {
|
||||
persistResource(
|
||||
new FeatureFlag.Builder()
|
||||
.setFeatureName(MINIMUM_DATASET_CONTACTS_PROHIBITED)
|
||||
.setStatusMap(
|
||||
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
|
||||
.build());
|
||||
void testFailure_minimumDataset_addingNewRegistrantFails() throws Exception {
|
||||
persistReferencedEntities();
|
||||
persistResource(
|
||||
DatabaseHelper.newDomain(getUniqueIdFromCommand())
|
||||
@@ -1593,109 +1392,6 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_removeTech() throws Exception {
|
||||
setEppInput("domain_update_remove_tech.xml");
|
||||
persistReferencedEntities();
|
||||
persistResource(
|
||||
DatabaseHelper.newDomain(getUniqueIdFromCommand())
|
||||
.asBuilder()
|
||||
.setContacts(
|
||||
ImmutableSet.of(
|
||||
DesignatedContact.create(Type.ADMIN, sh8013Contact.createVKey()),
|
||||
DesignatedContact.create(Type.TECH, sh8013Contact.createVKey())))
|
||||
.build());
|
||||
EppException thrown = assertThrows(MissingTechnicalContactException.class, this::runFlow);
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_minimumDatasetPhase1_removeTech() throws Exception {
|
||||
persistResource(
|
||||
new FeatureFlag.Builder()
|
||||
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
|
||||
.setStatusMap(
|
||||
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
|
||||
.build());
|
||||
setEppInput("domain_update_remove_tech.xml");
|
||||
persistReferencedEntities();
|
||||
persistResource(
|
||||
DatabaseHelper.newDomain(getUniqueIdFromCommand())
|
||||
.asBuilder()
|
||||
.setContacts(
|
||||
ImmutableSet.of(
|
||||
DesignatedContact.create(Type.ADMIN, sh8013Contact.createVKey()),
|
||||
DesignatedContact.create(Type.TECH, sh8013Contact.createVKey())))
|
||||
.build());
|
||||
runFlowAssertResponse(loadFile("generic_success_response.xml"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_minimumDatasetPhase2_removeAllContacts() throws Exception {
|
||||
persistResource(
|
||||
new FeatureFlag.Builder()
|
||||
.setFeatureName(MINIMUM_DATASET_CONTACTS_PROHIBITED)
|
||||
.setStatusMap(
|
||||
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
|
||||
.build());
|
||||
setEppInput("domain_update_remove_all_contacts.xml");
|
||||
persistReferencedEntities();
|
||||
persistResource(
|
||||
DatabaseHelper.newDomain(getUniqueIdFromCommand())
|
||||
.asBuilder()
|
||||
.setContacts(
|
||||
ImmutableSet.of(
|
||||
DesignatedContact.create(Type.ADMIN, sh8013Contact.createVKey()),
|
||||
DesignatedContact.create(Type.TECH, sh8013Contact.createVKey())))
|
||||
.build());
|
||||
runFlowAssertResponse(loadFile("generic_success_response.xml"));
|
||||
Domain updatedDomain = reloadResourceByForeignKey();
|
||||
assertThat(updatedDomain.getRegistrant()).isEmpty();
|
||||
assertThat(updatedDomain.getContacts()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_minimumDatasetPhase2_removeOneContact() throws Exception {
|
||||
persistResource(
|
||||
new FeatureFlag.Builder()
|
||||
.setFeatureName(MINIMUM_DATASET_CONTACTS_PROHIBITED)
|
||||
.setStatusMap(
|
||||
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, clock.nowUtc().minusDays(5), ACTIVE))
|
||||
.build());
|
||||
setEppInput("domain_update_remove_admin.xml");
|
||||
persistReferencedEntities();
|
||||
persistResource(
|
||||
DatabaseHelper.newDomain(getUniqueIdFromCommand())
|
||||
.asBuilder()
|
||||
.setContacts(
|
||||
ImmutableSet.of(
|
||||
DesignatedContact.create(Type.ADMIN, sh8013Contact.createVKey()),
|
||||
DesignatedContact.create(Type.TECH, sh8013Contact.createVKey())))
|
||||
.build());
|
||||
assertThat(reloadResourceByForeignKey().getRegistrant()).isPresent();
|
||||
assertThat(reloadResourceByForeignKey().getContacts()).hasSize(2);
|
||||
runFlowAssertResponse(loadFile("generic_success_response.xml"));
|
||||
Domain updatedDomain = reloadResourceByForeignKey();
|
||||
assertThat(updatedDomain.getRegistrant()).isPresent();
|
||||
assertThat(updatedDomain.getContacts()).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_addPendingDeleteContact() throws Exception {
|
||||
persistReferencedEntities();
|
||||
persistDomain();
|
||||
persistResource(
|
||||
loadResource(Contact.class, "mak21", clock.nowUtc())
|
||||
.get()
|
||||
.asBuilder()
|
||||
.addStatusValue(PENDING_DELETE)
|
||||
.build());
|
||||
clock.advanceOneMilli();
|
||||
LinkedResourceInPendingDeleteProhibitsOperationException thrown =
|
||||
assertThrows(LinkedResourceInPendingDeleteProhibitsOperationException.class, this::runFlow);
|
||||
assertThat(thrown).hasMessageThat().contains("mak21");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_addPendingDeleteHost() throws Exception {
|
||||
persistReferencedEntities();
|
||||
@@ -1727,31 +1423,6 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_changeContactsAndRegistrant() throws Exception {
|
||||
setEppInput("domain_update_contacts_and_registrant.xml");
|
||||
persistReferencedEntities();
|
||||
persistDomainWithRegistrant();
|
||||
|
||||
reloadResourceByForeignKey()
|
||||
.getContacts()
|
||||
.forEach(
|
||||
contact ->
|
||||
assertThat(loadByKey(contact.getContactKey()).getContactId()).isEqualTo("mak21"));
|
||||
assertThat(loadByKey(reloadResourceByForeignKey().getRegistrant().get()).getContactId())
|
||||
.isEqualTo("mak21");
|
||||
|
||||
runFlow();
|
||||
|
||||
reloadResourceByForeignKey()
|
||||
.getContacts()
|
||||
.forEach(
|
||||
contact ->
|
||||
assertThat(loadByKey(contact.getContactKey()).getContactId()).isEqualTo("sh8013"));
|
||||
assertThat(loadByKey(reloadResourceByForeignKey().getRegistrant().get()).getContactId())
|
||||
.isEqualTo("sh8013");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_tldWithNameserverAllowList_removeNameserver() throws Exception {
|
||||
setEppInput("domain_update_remove_nameserver.xml");
|
||||
|
||||
@@ -34,6 +34,7 @@ import google.registry.keyring.KeyringModule;
|
||||
import google.registry.keyring.api.KeyModule;
|
||||
import google.registry.module.TestRequestComponent.TestRequestComponentModule;
|
||||
import google.registry.monitoring.whitebox.StackdriverModule;
|
||||
import google.registry.mosapi.module.MosApiModule;
|
||||
import google.registry.persistence.PersistenceModule;
|
||||
import google.registry.privileges.secretmanager.SecretManagerModule;
|
||||
import google.registry.rde.JSchModule;
|
||||
@@ -61,6 +62,7 @@ import jakarta.inject.Singleton;
|
||||
GroupsModule.class,
|
||||
GroupssettingsModule.class,
|
||||
GsonModule.class,
|
||||
MosApiModule.class,
|
||||
JSchModule.class,
|
||||
KeyModule.class,
|
||||
KeyringModule.class,
|
||||
|
||||
203
core/src/test/java/google/registry/mosapi/MosApiClientTest.java
Normal file
203
core/src/test/java/google/registry/mosapi/MosApiClientTest.java
Normal file
@@ -0,0 +1,203 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.mosapi;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import google.registry.mosapi.MosApiException.MosApiAuthorizationException;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Protocol;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import okio.Buffer;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
public class MosApiClientTest {
|
||||
private static final String SERVICE_URL = "https://mosapi.example.com/v1";
|
||||
private static final String ENTITY_TYPE = "registries";
|
||||
|
||||
// Mocks
|
||||
private OkHttpClient mockHttpClient;
|
||||
private Call mockCall;
|
||||
|
||||
private MosApiClient mosApiClient;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockHttpClient = mock(OkHttpClient.class);
|
||||
mockCall = mock(Call.class);
|
||||
when(mockHttpClient.newCall(any(Request.class))).thenReturn(mockCall);
|
||||
mosApiClient = new MosApiClient(mockHttpClient, SERVICE_URL, ENTITY_TYPE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructor_throwsOnInvalidUrl() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> new MosApiClient(mockHttpClient, "ht tp://bad-url", ENTITY_TYPE));
|
||||
assertThat(thrown).hasMessageThat().contains("Invalid MoSAPI Service URL");
|
||||
}
|
||||
|
||||
// --- GET Request Tests ---
|
||||
|
||||
@Test
|
||||
void testSendGetRequest_success() throws Exception {
|
||||
// 1. Prepare Success Response
|
||||
Response successResponse = createResponse(200, "{\"status\":\"ok\"}");
|
||||
when(mockCall.execute()).thenReturn(successResponse);
|
||||
|
||||
Map<String, String> params = ImmutableMap.of("since", "2024-01-01");
|
||||
Map<String, String> headers = ImmutableMap.of("Authorization", "Bearer token123");
|
||||
|
||||
// 2. Execute
|
||||
try (Response response =
|
||||
mosApiClient.sendGetRequest("tld-1", "monitoring/state", params, headers)) {
|
||||
|
||||
// Verify Response
|
||||
assertThat(response.isSuccessful()).isTrue();
|
||||
assertThat(response.body().string()).isEqualTo("{\"status\":\"ok\"}");
|
||||
|
||||
// 3. Verify Request Construction
|
||||
ArgumentCaptor<Request> requestCaptor = ArgumentCaptor.forClass(Request.class);
|
||||
verify(mockHttpClient).newCall(requestCaptor.capture());
|
||||
Request capturedRequest = requestCaptor.getValue();
|
||||
|
||||
// Check URL:
|
||||
assertThat(capturedRequest.method()).isEqualTo("GET");
|
||||
assertThat(capturedRequest.url().encodedPath())
|
||||
.isEqualTo("/v1/registries/tld-1/monitoring/state");
|
||||
assertThat(capturedRequest.url().queryParameter("since")).isEqualTo("2024-01-01");
|
||||
|
||||
// Check Headers
|
||||
assertThat(capturedRequest.header("Authorization")).isEqualTo("Bearer token123");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendGetRequest_throwsOn401() throws IOException {
|
||||
// Prepare 401 Response
|
||||
Response unauthorizedResponse = createResponse(401, "Unauthorized");
|
||||
when(mockCall.execute()).thenReturn(unauthorizedResponse);
|
||||
|
||||
MosApiAuthorizationException thrown =
|
||||
assertThrows(
|
||||
MosApiAuthorizationException.class,
|
||||
() ->
|
||||
mosApiClient.sendGetRequest("tld-1", "path", ImmutableMap.of(), ImmutableMap.of()));
|
||||
|
||||
assertThat(thrown).hasMessageThat().contains("Authorization failed");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendGetRequest_wrapsIoException() throws IOException {
|
||||
// Simulate Network Failure
|
||||
when(mockCall.execute()).thenThrow(new IOException("Network error"));
|
||||
|
||||
// Execute & Assert
|
||||
MosApiException thrown =
|
||||
assertThrows(
|
||||
MosApiException.class,
|
||||
() ->
|
||||
mosApiClient.sendGetRequest("tld-1", "path", ImmutableMap.of(), ImmutableMap.of()));
|
||||
|
||||
assertThat(thrown).hasMessageThat().contains("Error during GET request to");
|
||||
assertThat(thrown).hasCauseThat().isInstanceOf(IOException.class);
|
||||
}
|
||||
|
||||
// --- POST Request Tests ---
|
||||
|
||||
@Test
|
||||
void testSendPostRequest_success() throws Exception {
|
||||
// 1. Prepare Response
|
||||
Response successResponse = createResponse(200, "{\"updated\":true}");
|
||||
when(mockCall.execute()).thenReturn(successResponse);
|
||||
|
||||
String requestBody = "{\"data\":\"update\"}";
|
||||
Map<String, String> headers = ImmutableMap.of("Content-Type", "application/json");
|
||||
|
||||
try (Response response =
|
||||
mosApiClient.sendPostRequest("tld-1", "update", null, headers, requestBody)) {
|
||||
|
||||
assertThat(response.isSuccessful()).isTrue();
|
||||
|
||||
ArgumentCaptor<Request> requestCaptor = ArgumentCaptor.forClass(Request.class);
|
||||
verify(mockHttpClient).newCall(requestCaptor.capture());
|
||||
Request capturedRequest = requestCaptor.getValue();
|
||||
|
||||
assertThat(capturedRequest.method()).isEqualTo("POST");
|
||||
assertThat(capturedRequest.url().encodedPath()).isEqualTo("/v1/registries/tld-1/update");
|
||||
|
||||
// Verify Body content
|
||||
Buffer buffer = new Buffer();
|
||||
capturedRequest.body().writeTo(buffer);
|
||||
assertThat(buffer.readUtf8()).isEqualTo(requestBody);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendPostRequest_throwsOn401() throws IOException {
|
||||
// Prepare 401 Response
|
||||
Response unauthorizedResponse = createResponse(401, "Unauthorized");
|
||||
when(mockCall.execute()).thenReturn(unauthorizedResponse);
|
||||
|
||||
MosApiAuthorizationException thrown =
|
||||
assertThrows(
|
||||
MosApiAuthorizationException.class,
|
||||
() ->
|
||||
mosApiClient.sendPostRequest(
|
||||
"tld-1", "path", ImmutableMap.of(), ImmutableMap.of(), "{}"));
|
||||
|
||||
assertThat(thrown).hasMessageThat().contains("Authorization failed");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendPostRequest_wrapsIoException() throws IOException {
|
||||
// Simulate Network Failure
|
||||
when(mockCall.execute()).thenThrow(new IOException("Network error"));
|
||||
MosApiException thrown =
|
||||
assertThrows(
|
||||
MosApiException.class,
|
||||
() ->
|
||||
mosApiClient.sendPostRequest(
|
||||
"tld-1", "path", ImmutableMap.of(), ImmutableMap.of(), "{}"));
|
||||
assertThat(thrown).hasMessageThat().contains("Error during POST request to");
|
||||
assertThat(thrown).hasCauseThat().isInstanceOf(IOException.class);
|
||||
}
|
||||
|
||||
/** Helper to build a real OkHttp Response object manually. */
|
||||
private Response createResponse(int code, String bodyContent) {
|
||||
return new Response.Builder()
|
||||
.request(new Request.Builder().url("http://localhost/").build())
|
||||
.protocol(Protocol.HTTP_1_1)
|
||||
.code(code)
|
||||
.message("Msg")
|
||||
.body(ResponseBody.create(bodyContent, MediaType.parse("application/json")))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package google.registry.mosapi;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import google.registry.mosapi.MosApiException.DateDurationInvalidException;
|
||||
import google.registry.mosapi.MosApiException.DateOrderInvalidException;
|
||||
import google.registry.mosapi.MosApiException.EndDateSyntaxInvalidException;
|
||||
import google.registry.mosapi.MosApiException.MosApiAuthorizationException;
|
||||
import google.registry.mosapi.MosApiException.StartDateSyntaxInvalidException;
|
||||
import google.registry.mosapi.model.MosApiErrorResponse;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link MosApiException}. */
|
||||
public class MosApiExceptionTest {
|
||||
|
||||
@Test
|
||||
void testConstructor_withErrorResponse() {
|
||||
MosApiErrorResponse errorResponse =
|
||||
new MosApiErrorResponse("1234", "Test Message", "Test Description");
|
||||
MosApiException exception = new MosApiException(errorResponse);
|
||||
assertThat(exception)
|
||||
.hasMessageThat()
|
||||
.isEqualTo("MoSAPI returned an error (code: 1234): Test Message");
|
||||
assertThat(exception.getErrorResponse()).hasValue(errorResponse);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructor_withMessageAndCause() {
|
||||
RuntimeException cause = new RuntimeException("Root Cause");
|
||||
MosApiException exception = new MosApiException("Wrapper Message", cause);
|
||||
assertThat(exception).hasMessageThat().isEqualTo("Wrapper Message");
|
||||
assertThat(exception).hasCauseThat().isEqualTo(cause);
|
||||
assertThat(exception.getErrorResponse()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAuthorizationException() {
|
||||
MosApiAuthorizationException exception = new MosApiAuthorizationException("Unauthorized");
|
||||
assertThat(exception).isInstanceOf(MosApiException.class);
|
||||
assertThat(exception).hasMessageThat().isEqualTo("Unauthorized");
|
||||
assertThat(exception.getErrorResponse()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreate_forDateDurationInvalid() {
|
||||
MosApiErrorResponse errorResponse =
|
||||
new MosApiErrorResponse("2011", "Duration invalid", "Description");
|
||||
MosApiException exception = MosApiException.create(errorResponse);
|
||||
assertThat(exception).isInstanceOf(DateDurationInvalidException.class);
|
||||
assertThat(exception.getErrorResponse()).hasValue(errorResponse);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreate_forDateOrderInvalid() {
|
||||
MosApiErrorResponse errorResponse =
|
||||
new MosApiErrorResponse("2012", "End date before start date", "Description");
|
||||
MosApiException exception = MosApiException.create(errorResponse);
|
||||
assertThat(exception).isInstanceOf(DateOrderInvalidException.class);
|
||||
assertThat(exception.getErrorResponse()).hasValue(errorResponse);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreate_forStartDateSyntaxInvalid() {
|
||||
MosApiErrorResponse errorResponse =
|
||||
new MosApiErrorResponse("2013", "Invalid start date format", "Description");
|
||||
MosApiException exception = MosApiException.create(errorResponse);
|
||||
assertThat(exception).isInstanceOf(StartDateSyntaxInvalidException.class);
|
||||
assertThat(exception.getErrorResponse()).hasValue(errorResponse);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreate_forEndDateSyntaxInvalid() {
|
||||
MosApiErrorResponse errorResponse =
|
||||
new MosApiErrorResponse("2014", "Invalid end date format", "Description");
|
||||
MosApiException exception = MosApiException.create(errorResponse);
|
||||
assertThat(exception).isInstanceOf(EndDateSyntaxInvalidException.class);
|
||||
assertThat(exception.getErrorResponse()).hasValue(errorResponse);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreate_forUnknownCode() {
|
||||
MosApiErrorResponse errorResponse = new MosApiErrorResponse("9999", "Unknown", "Description");
|
||||
MosApiException exception = MosApiException.create(errorResponse);
|
||||
assertThat(exception.getClass()).isEqualTo(MosApiException.class);
|
||||
assertThat(exception.getErrorResponse()).hasValue(errorResponse);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package google.registry.mosapi.model;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link MosApiErrorResponse}. */
|
||||
public class MosApiErrorResponseTest {
|
||||
|
||||
@Test
|
||||
void testJsonDeserialization() {
|
||||
String json =
|
||||
"""
|
||||
{
|
||||
"resultCode": "2012",
|
||||
"message": "The endDate is before the startDate.",
|
||||
"description": "Validation failed"
|
||||
}
|
||||
""";
|
||||
|
||||
MosApiErrorResponse response = new Gson().fromJson(json, MosApiErrorResponse.class);
|
||||
|
||||
assertThat(response.resultCode()).isEqualTo("2012");
|
||||
assertThat(response.message()).isEqualTo("The endDate is before the startDate.");
|
||||
assertThat(response.description()).isEqualTo("Validation failed");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.mosapi.module;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import google.registry.privileges.secretmanager.SecretManagerClient;
|
||||
import java.io.StringWriter;
|
||||
import java.math.BigInteger;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.KeyStore;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.Security;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Optional;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
import okhttp3.OkHttpClient;
|
||||
import org.bouncycastle.asn1.ASN1GeneralizedTime;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
|
||||
import org.bouncycastle.asn1.x509.Time;
|
||||
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
|
||||
import org.bouncycastle.operator.ContentSigner;
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class MosApiModuleTest {
|
||||
|
||||
private static final String TEST_CERT_SECRET_NAME = "testCert";
|
||||
private static final String TEST_KEY_SECRET_NAME = "testKey";
|
||||
|
||||
private SecretManagerClient secretManagerClient;
|
||||
private String validCertPem;
|
||||
private String validKeyPem;
|
||||
private PrivateKey generatedPrivateKey;
|
||||
private X509Certificate generatedCertificate;
|
||||
|
||||
@BeforeAll
|
||||
static void setupStatics() {
|
||||
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
secretManagerClient = mock(SecretManagerClient.class);
|
||||
generateTestCredentials();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProvideMosapiTlsCert_fetchesFromConfiguredSecretName() {
|
||||
when(secretManagerClient.getSecretData(any(), any())).thenReturn(validCertPem);
|
||||
String result = MosApiModule.provideMosapiTlsCert(secretManagerClient, TEST_CERT_SECRET_NAME);
|
||||
assertThat(result).isEqualTo(validCertPem);
|
||||
verify(secretManagerClient).getSecretData(eq(TEST_CERT_SECRET_NAME), eq(Optional.of("latest")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProvideMosapiTlsKey_fetchesFromConfiguredSecretName() {
|
||||
when(secretManagerClient.getSecretData(any(), any())).thenReturn(validKeyPem);
|
||||
String result = MosApiModule.provideMosapiTlsKey(secretManagerClient, TEST_KEY_SECRET_NAME);
|
||||
assertThat(result).isEqualTo(validKeyPem);
|
||||
verify(secretManagerClient).getSecretData(eq(TEST_KEY_SECRET_NAME), eq(Optional.of("latest")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProvideCertificate_parsesValidPem() {
|
||||
Certificate cert = MosApiModule.provideCertificate(validCertPem);
|
||||
assertThat(cert).isInstanceOf(X509Certificate.class);
|
||||
// Verify the public key matches to ensure we parsed the correct cert
|
||||
assertThat(cert.getPublicKey()).isEqualTo(generatedCertificate.getPublicKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProvideCertificate_throwsOnInvalidPem() {
|
||||
RuntimeException thrown =
|
||||
assertThrows(
|
||||
RuntimeException.class, () -> MosApiModule.provideCertificate("NOT A REAL CERT"));
|
||||
assertThat(thrown).hasMessageThat().contains("Could not create X.509 certificate");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProvidePrivateKey_parsesValidPem() {
|
||||
PrivateKey key = MosApiModule.providePrivateKey(validKeyPem);
|
||||
assertThat(key).isNotNull();
|
||||
assertThat(key.getAlgorithm()).isEqualTo("RSA");
|
||||
assertThat(key.getEncoded()).isEqualTo(generatedPrivateKey.getEncoded());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProvidePrivateKey_throwsOnInvalidPem() {
|
||||
RuntimeException thrown =
|
||||
assertThrows(
|
||||
RuntimeException.class, () -> MosApiModule.providePrivateKey("NOT A REAL KEY"));
|
||||
assertThat(thrown).hasMessageThat().contains("Could not parse TLS private key");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProvideKeyStore_createsWithCorrectAlias() throws Exception {
|
||||
KeyStore keyStore = MosApiModule.provideKeyStore(generatedPrivateKey, generatedCertificate);
|
||||
assertThat(keyStore).isNotNull();
|
||||
assertThat(keyStore.getType()).isEqualTo("PKCS12");
|
||||
assertThat(keyStore.containsAlias("client")).isTrue();
|
||||
assertThat(keyStore.getCertificate("client")).isEqualTo(generatedCertificate);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProvideMosapiHttpClient_usesConfiguredSslContext() {
|
||||
SSLContext mockSslContext = mock(SSLContext.class);
|
||||
SSLSocketFactory mockSocketFactory = mock(SSLSocketFactory.class);
|
||||
X509TrustManager mockTrustManager = mock(X509TrustManager.class);
|
||||
when(mockTrustManager.getAcceptedIssuers()).thenReturn(new X509Certificate[0]);
|
||||
when(mockSslContext.getSocketFactory()).thenReturn(mockSocketFactory);
|
||||
OkHttpClient client = MosApiModule.provideMosapiHttpClient(mockSslContext, mockTrustManager);
|
||||
assertThat(client).isNotNull();
|
||||
assertThat(client.sslSocketFactory()).isEqualTo(mockSocketFactory);
|
||||
}
|
||||
|
||||
private void generateTestCredentials() throws Exception {
|
||||
// 1. Generate KeyPair
|
||||
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
|
||||
keyGen.initialize(2048);
|
||||
KeyPair keyPair = keyGen.generateKeyPair();
|
||||
this.generatedPrivateKey = keyPair.getPrivate();
|
||||
DateTimeFormatter formatter =
|
||||
DateTimeFormatter.ofPattern("yyyyMMddHHmmss'Z'").withZone(ZoneId.of("UTC"));
|
||||
Instant now = Instant.now();
|
||||
Instant end = now.plus(Duration.ofDays(365));
|
||||
// Convert string to Bouncy Castle Time objects
|
||||
Time notBefore = new Time(new ASN1GeneralizedTime(formatter.format(now)));
|
||||
Time notAfter = new Time(new ASN1GeneralizedTime(formatter.format(end)));
|
||||
X509v3CertificateBuilder certBuilder =
|
||||
new X509v3CertificateBuilder(
|
||||
new X500Name("CN=Test"),
|
||||
BigInteger.valueOf(now.toEpochMilli()),
|
||||
notBefore,
|
||||
notAfter,
|
||||
new X500Name("CN=Test"),
|
||||
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
|
||||
ContentSigner contentSigner =
|
||||
new JcaContentSignerBuilder("SHA256WithRSAEncryption").build(keyPair.getPrivate());
|
||||
this.generatedCertificate =
|
||||
new JcaX509CertificateConverter()
|
||||
.setProvider("BC")
|
||||
.getCertificate(certBuilder.build(contentSigner));
|
||||
// 4. Convert to PEM Strings
|
||||
this.validCertPem = toPem(generatedCertificate);
|
||||
this.validKeyPem = toPem(generatedPrivateKey);
|
||||
}
|
||||
|
||||
private String toPem(Object object) throws Exception {
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
try (JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter)) {
|
||||
pemWriter.writeObject(object);
|
||||
}
|
||||
return stringWriter.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.tools;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.io.CharSink;
|
||||
import com.google.common.io.Files;
|
||||
import com.google.common.net.MediaType;
|
||||
import java.io.File;
|
||||
import java.util.stream.IntStream;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
/** Tests fir {@link BulkDomainTransferCommand}. */
|
||||
public class BulkDomainTransferCommandTest extends CommandTestCase<BulkDomainTransferCommand> {
|
||||
|
||||
private ServiceConnection connection;
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
connection = mock(ServiceConnection.class);
|
||||
command.setConnection(connection);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_validParametersSent() throws Exception {
|
||||
runCommandForced(
|
||||
"--gaining_registrar_id", "NewRegistrar",
|
||||
"--losing_registrar_id", "TheRegistrar",
|
||||
"--reason", "someReason",
|
||||
"--domains", "foo.tld,bar.tld");
|
||||
assertInStdout("Sending batch of 2 domains");
|
||||
verify(connection)
|
||||
.sendPostRequest(
|
||||
"/_dr/task/bulkDomainTransfer",
|
||||
ImmutableMap.of(
|
||||
"gainingRegistrarId",
|
||||
"NewRegistrar",
|
||||
"losingRegistrarId",
|
||||
"TheRegistrar",
|
||||
"requestedByRegistrar",
|
||||
false,
|
||||
"reason",
|
||||
"someReason"),
|
||||
MediaType.PLAIN_TEXT_UTF_8,
|
||||
"[\"foo.tld\",\"bar.tld\"]".getBytes(UTF_8));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_fileInBatches() throws Exception {
|
||||
File domainNamesFile = tmpDir.resolve("domain_names.txt").toFile();
|
||||
CharSink sink = Files.asCharSink(domainNamesFile, UTF_8);
|
||||
sink.writeLines(IntStream.range(0, 1003).mapToObj(i -> String.format("foo%d.tld", i)));
|
||||
runCommandForced(
|
||||
"--gaining_registrar_id", "NewRegistrar",
|
||||
"--losing_registrar_id", "TheRegistrar",
|
||||
"--reason", "someReason",
|
||||
"--domain_names_file", domainNamesFile.getPath());
|
||||
assertInStdout("Sending batch of 1000 domains");
|
||||
assertInStdout("Sending batch of 3 domains");
|
||||
ArgumentCaptor<byte[]> listCaptor = ArgumentCaptor.forClass(byte[].class);
|
||||
verify(connection, times(2))
|
||||
.sendPostRequest(
|
||||
eq("/_dr/task/bulkDomainTransfer"),
|
||||
eq(
|
||||
ImmutableMap.of(
|
||||
"gainingRegistrarId",
|
||||
"NewRegistrar",
|
||||
"losingRegistrarId",
|
||||
"TheRegistrar",
|
||||
"requestedByRegistrar",
|
||||
false,
|
||||
"reason",
|
||||
"someReason")),
|
||||
eq(MediaType.PLAIN_TEXT_UTF_8),
|
||||
listCaptor.capture());
|
||||
assertThat(listCaptor.getValue())
|
||||
.isEqualTo("[\"foo1000.tld\",\"foo1001.tld\",\"foo1002.tld\"]".getBytes(UTF_8));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_badGaining() {
|
||||
assertThat(
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
runCommandForced(
|
||||
"--gaining_registrar_id", "Bad",
|
||||
"--losing_registrar_id", "TheRegistrar",
|
||||
"--reason", "someReason",
|
||||
"--domains", "foo.tld,baz.tld")))
|
||||
.hasMessageThat()
|
||||
.isEqualTo("Gaining registrar Bad doesn't exist");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_badLosing() {
|
||||
assertThat(
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
runCommandForced(
|
||||
"--gaining_registrar_id", "NewRegistrar",
|
||||
"--losing_registrar_id", "Bad",
|
||||
"--reason", "someReason",
|
||||
"--domains", "foo.tld,baz.tld")))
|
||||
.hasMessageThat()
|
||||
.isEqualTo("Losing registrar Bad doesn't exist");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_noDomainsSpecified() {
|
||||
assertThat(
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
runCommandForced(
|
||||
"--gaining_registrar_id", "NewRegistrar",
|
||||
"--losing_registrar_id", "TheRegistrar",
|
||||
"--reason", "someReason")))
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
"Must specify exactly one input method, either --domains or --domain_names_file");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_bothDomainMethodsSpecified() {
|
||||
assertThat(
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
runCommandForced(
|
||||
"--gaining_registrar_id",
|
||||
"NewRegistrar",
|
||||
"--losing_registrar_id",
|
||||
"TheRegistrar",
|
||||
"--reason",
|
||||
"someReason",
|
||||
"--domains",
|
||||
"foo.tld,baz.tld",
|
||||
"--domain_names_file",
|
||||
"foo.txt")))
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
"Must specify exactly one input method, either --domains or --domain_names_file");
|
||||
}
|
||||
}
|
||||
@@ -15,22 +15,16 @@
|
||||
package google.registry.tools;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureStatus.ACTIVE;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.testing.DatabaseHelper.createTld;
|
||||
import static google.registry.testing.DatabaseHelper.persistPremiumList;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
import static org.joda.money.CurrencyUnit.JPY;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import com.beust.jcommander.ParameterException;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import google.registry.dns.writer.VoidDnsWriter;
|
||||
import google.registry.model.common.FeatureFlag;
|
||||
import google.registry.model.pricing.StaticPremiumListPricingEngine;
|
||||
import google.registry.model.tld.Tld;
|
||||
import google.registry.model.tld.label.PremiumListDao;
|
||||
@@ -116,26 +110,7 @@ class CreateDomainCommandTest extends EppToolCommandTestCase<CreateDomainCommand
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_minimumDatasetPhase1_noContacts() throws Exception {
|
||||
persistResource(
|
||||
new FeatureFlag()
|
||||
.asBuilder()
|
||||
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
|
||||
.setStatusMap(ImmutableSortedMap.of(START_OF_TIME, ACTIVE))
|
||||
.build());
|
||||
// Test that each optional field can be omitted. Also tests the auto-gen password.
|
||||
runCommandForced("--client=NewRegistrar", "example.tld");
|
||||
eppVerifier.verifySent("domain_create_minimal.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_minimumDatasetPhase2_noContacts() throws Exception {
|
||||
persistResource(
|
||||
new FeatureFlag()
|
||||
.asBuilder()
|
||||
.setFeatureName(MINIMUM_DATASET_CONTACTS_PROHIBITED)
|
||||
.setStatusMap(ImmutableSortedMap.of(START_OF_TIME, ACTIVE))
|
||||
.build());
|
||||
void testSuccess_minimumDataset_noContacts() throws Exception {
|
||||
// Test that each optional field can be omitted. Also tests the auto-gen password.
|
||||
runCommandForced(
|
||||
"--client=NewRegistrar",
|
||||
@@ -308,48 +283,6 @@ class CreateDomainCommandTest extends EppToolCommandTestCase<CreateDomainCommand
|
||||
assertThat(thrown).hasMessageThat().contains("--client");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_missingRegistrant() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
runCommandForced(
|
||||
"--client=NewRegistrar",
|
||||
"--admins=crr-admin",
|
||||
"--techs=crr-tech",
|
||||
"example.tld"));
|
||||
assertThat(thrown).hasMessageThat().contains("Registrant must be specified");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_missingAdmins() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
runCommandForced(
|
||||
"--client=NewRegistrar",
|
||||
"--registrant=crr-admin",
|
||||
"--techs=crr-tech",
|
||||
"example.tld"));
|
||||
assertThat(thrown).hasMessageThat().contains("At least one admin must be specified");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_missingTechs() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
runCommandForced(
|
||||
"--client=NewRegistrar",
|
||||
"--registrant=crr-admin",
|
||||
"--admins=crr-admin",
|
||||
"example.tld"));
|
||||
assertThat(thrown).hasMessageThat().contains("At least one tech must be specified");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_tooManyNameServers() {
|
||||
IllegalArgumentException thrown =
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.tools;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureName.TEST_FEATURE;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureStatus.ACTIVE;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import google.registry.model.common.FeatureFlag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Tests for {@link DeleteFeatureFlagCommand}. */
|
||||
public class DeleteFeatureFlagCommandTest extends CommandTestCase<DeleteFeatureFlagCommand> {
|
||||
|
||||
@Test
|
||||
void testSimpleSuccess() throws Exception {
|
||||
persistResource(
|
||||
new FeatureFlag()
|
||||
.asBuilder()
|
||||
.setFeatureName(TEST_FEATURE)
|
||||
.setStatusMap(ImmutableSortedMap.of(START_OF_TIME, ACTIVE))
|
||||
.build());
|
||||
assertThat(tm().transact(() -> FeatureFlag.isActiveNow(TEST_FEATURE))).isTrue();
|
||||
runCommandForced("TEST_FEATURE");
|
||||
assertThat(FeatureFlag.getUncached(TEST_FEATURE)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_noLongerPartOfEnum() throws Exception {
|
||||
tm().transact(
|
||||
() ->
|
||||
tm().getEntityManager()
|
||||
.createNativeQuery(
|
||||
"INSERT INTO \"FeatureFlag\" VALUES('nonexistent',"
|
||||
+ " '\"1970-01-01T00:00:00.000Z\"=>\"INACTIVE\"')")
|
||||
.executeUpdate());
|
||||
assertThat(
|
||||
tm().transact(
|
||||
() ->
|
||||
tm().query(
|
||||
"SELECT COUNT(*) FROM FeatureFlag WHERE featureName ="
|
||||
+ " 'nonexistent'",
|
||||
long.class)
|
||||
.getSingleResult()))
|
||||
.isEqualTo(1L);
|
||||
runCommandForced("nonexistent");
|
||||
assertThat(
|
||||
tm().transact(
|
||||
() ->
|
||||
tm().query(
|
||||
"SELECT COUNT(*) FROM FeatureFlag WHERE featureName ="
|
||||
+ " 'nonexistent'",
|
||||
long.class)
|
||||
.getSingleResult()))
|
||||
.isEqualTo(0L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_nonExistent() throws Exception {
|
||||
runCommandForced("nonexistent");
|
||||
assertInStdout("No flag found with name 'nonexistent'");
|
||||
}
|
||||
}
|
||||
@@ -14,23 +14,17 @@
|
||||
|
||||
package google.registry.tools;
|
||||
|
||||
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureStatus.INACTIVE;
|
||||
import static google.registry.testing.DatabaseHelper.assertBillingEventsForResource;
|
||||
import static google.registry.testing.DatabaseHelper.createTlds;
|
||||
import static google.registry.testing.DatabaseHelper.getOnlyHistoryEntryOfType;
|
||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import google.registry.flows.EppTestCase;
|
||||
import google.registry.model.ForeignKeyUtils;
|
||||
import google.registry.model.billing.BillingBase.Reason;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.common.FeatureFlag;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.DomainHistory;
|
||||
import google.registry.model.reporting.HistoryEntry.Type;
|
||||
@@ -59,19 +53,12 @@ class EppLifecycleToolsTest extends EppTestCase {
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
persistResource(
|
||||
new FeatureFlag()
|
||||
.asBuilder()
|
||||
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
|
||||
.setStatusMap(ImmutableSortedMap.of(START_OF_TIME, INACTIVE))
|
||||
.build());
|
||||
createTlds("example", "tld");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_renewDomainThenUnrenew() throws Exception {
|
||||
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
|
||||
createContacts(DateTime.parse("2000-06-01T00:00:00Z"));
|
||||
|
||||
// Create the domain for 2 years.
|
||||
assertThatCommand(
|
||||
@@ -128,7 +115,7 @@ class EppLifecycleToolsTest extends EppTestCase {
|
||||
.atTime("2001-06-08T00:00:00Z")
|
||||
.hasResponse("poll_response_unrenew.xml");
|
||||
|
||||
assertThatCommand("poll_ack.xml", ImmutableMap.of("ID", "21-2001"))
|
||||
assertThatCommand("poll_ack.xml", ImmutableMap.of("ID", "17-2001"))
|
||||
.atTime("2001-06-08T00:00:01Z")
|
||||
.hasResponse("poll_ack_response_empty.xml");
|
||||
|
||||
@@ -149,7 +136,7 @@ class EppLifecycleToolsTest extends EppTestCase {
|
||||
.hasResponse(
|
||||
"poll_response_autorenew.xml",
|
||||
ImmutableMap.of(
|
||||
"ID", "23-2003",
|
||||
"ID", "19-2003",
|
||||
"QDATE", "2003-06-01T00:02:00Z",
|
||||
"DOMAIN", "example.tld",
|
||||
"EXDATE", "2004-06-01T00:02:00Z"));
|
||||
|
||||
@@ -15,14 +15,10 @@
|
||||
package google.registry.ui.server.console.domains;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL;
|
||||
import static google.registry.model.common.FeatureFlag.FeatureStatus.INACTIVE;
|
||||
import static google.registry.testing.DatabaseHelper.loadByEntity;
|
||||
import static google.registry.testing.DatabaseHelper.loadSingleton;
|
||||
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.util.DateTimeUtils.START_OF_TIME;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
|
||||
@@ -31,12 +27,10 @@ import static org.mockito.Mockito.when;
|
||||
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.gson.JsonElement;
|
||||
import google.registry.flows.DaggerEppTestComponent;
|
||||
import google.registry.flows.EppController;
|
||||
import google.registry.flows.EppTestComponent;
|
||||
import google.registry.model.common.FeatureFlag;
|
||||
import google.registry.model.console.ConsoleUpdateHistory;
|
||||
import google.registry.model.console.RegistrarRole;
|
||||
import google.registry.model.console.User;
|
||||
@@ -68,12 +62,6 @@ public class ConsoleBulkDomainActionTest extends ConsoleActionBaseTestCase {
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
persistResource(
|
||||
new FeatureFlag()
|
||||
.asBuilder()
|
||||
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
|
||||
.setStatusMap(ImmutableSortedMap.of(START_OF_TIME, INACTIVE))
|
||||
.build());
|
||||
eppController =
|
||||
DaggerEppTestComponent.builder()
|
||||
.fakesAndMocksModule(EppTestComponent.FakesAndMocksModule.create(clock))
|
||||
@@ -84,7 +72,7 @@ public class ConsoleBulkDomainActionTest extends ConsoleActionBaseTestCase {
|
||||
persistDomainWithDependentResources(
|
||||
"example",
|
||||
"tld",
|
||||
persistActiveContact("contact1234"),
|
||||
null,
|
||||
clock.nowUtc(),
|
||||
clock.nowUtc().minusMonths(1),
|
||||
clock.nowUtc().plusMonths(11));
|
||||
@@ -101,9 +89,10 @@ public class ConsoleBulkDomainActionTest extends ConsoleActionBaseTestCase {
|
||||
assertThat(response.getStatus()).isEqualTo(SC_OK);
|
||||
assertThat(response.getPayload())
|
||||
.isEqualTo(
|
||||
"""
|
||||
"""
|
||||
{"example.tld":{"message":"Command completed successfully; action pending",\
|
||||
"responseCode":1001}}""");
|
||||
"responseCode":1001}}\
|
||||
""");
|
||||
assertThat(loadByEntity(domain).getDeletionTime()).isEqualTo(clock.nowUtc().plusDays(35));
|
||||
ConsoleUpdateHistory history = loadSingleton(ConsoleUpdateHistory.class).get();
|
||||
assertThat(history.getType()).isEqualTo(ConsoleUpdateHistory.Type.DOMAIN_DELETE);
|
||||
@@ -122,7 +111,8 @@ public class ConsoleBulkDomainActionTest extends ConsoleActionBaseTestCase {
|
||||
assertThat(response.getPayload())
|
||||
.isEqualTo(
|
||||
"""
|
||||
{"example.tld":{"message":"Command completed successfully","responseCode":1000}}""");
|
||||
{"example.tld":{"message":"Command completed successfully","responseCode":1000}}\
|
||||
""");
|
||||
assertThat(loadByEntity(domain).getStatusValues())
|
||||
.containsAtLeastElementsIn(serverSuspensionStatuses);
|
||||
ConsoleUpdateHistory history = loadSingleton(ConsoleUpdateHistory.class).get();
|
||||
@@ -145,7 +135,8 @@ public class ConsoleBulkDomainActionTest extends ConsoleActionBaseTestCase {
|
||||
assertThat(response.getPayload())
|
||||
.isEqualTo(
|
||||
"""
|
||||
{"example.tld":{"message":"Command completed successfully","responseCode":1000}}""");
|
||||
{"example.tld":{"message":"Command completed successfully","responseCode":1000}}\
|
||||
""");
|
||||
assertThat(loadByEntity(domain).getStatusValues()).containsNoneIn(serverSuspensionStatuses);
|
||||
ConsoleUpdateHistory history = loadSingleton(ConsoleUpdateHistory.class).get();
|
||||
assertThat(history.getType()).isEqualTo(ConsoleUpdateHistory.Type.DOMAIN_UNSUSPEND);
|
||||
@@ -167,10 +158,11 @@ public class ConsoleBulkDomainActionTest extends ConsoleActionBaseTestCase {
|
||||
assertThat(response.getStatus()).isEqualTo(SC_OK);
|
||||
assertThat(response.getPayload())
|
||||
.isEqualTo(
|
||||
"""
|
||||
"""
|
||||
{"example.tld":{"message":"Command completed successfully; action pending","responseCode":1001},\
|
||||
"nonexistent.tld":{"message":"The domain with given ID (nonexistent.tld) doesn\\u0027t exist.",\
|
||||
"responseCode":2303}}""");
|
||||
"responseCode":2303}}\
|
||||
""");
|
||||
assertThat(loadByEntity(domain).getDeletionTime()).isEqualTo(clock.nowUtc().plusDays(35));
|
||||
ConsoleUpdateHistory history = loadSingleton(ConsoleUpdateHistory.class).get();
|
||||
assertThat(history.getType()).isEqualTo(ConsoleUpdateHistory.Type.DOMAIN_DELETE);
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
|
||||
<response>
|
||||
<result code="1001">
|
||||
<msg>Command completed successfully; action pending</msg>
|
||||
</result>
|
||||
<trID>
|
||||
<svTRID>server-trid</svTRID>
|
||||
</trID>
|
||||
</response>
|
||||
</epp>
|
||||
@@ -1,11 +0,0 @@
|
||||
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
|
||||
<response>
|
||||
<result code="1001">
|
||||
<msg>Command completed successfully; action pending</msg>
|
||||
</result>
|
||||
<trID>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
<svTRID>server-trid</svTRID>
|
||||
</trID>
|
||||
</response>
|
||||
</epp>
|
||||
@@ -1,11 +0,0 @@
|
||||
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
|
||||
<response>
|
||||
<result code="1001">
|
||||
<msg>Command completed successfully; action pending</msg>
|
||||
</result>
|
||||
<trID>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
<svTRID>server-trid</svTRID>
|
||||
</trID>
|
||||
</response>
|
||||
</epp>
|
||||
@@ -1,22 +0,0 @@
|
||||
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
|
||||
<command>
|
||||
<check>
|
||||
<domain:check
|
||||
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
|
||||
<domain:name>collision.tld</domain:name>
|
||||
<domain:name>reserved.tld</domain:name>
|
||||
<domain:name>anchor.tld</domain:name>
|
||||
<domain:name>allowedinsunrise.tld</domain:name>
|
||||
<domain:name>premiumcollision.tld</domain:name>
|
||||
</domain:check>
|
||||
</check>
|
||||
<extension>
|
||||
<allocationToken:allocationToken
|
||||
xmlns:allocationToken=
|
||||
"urn:ietf:params:xml:ns:allocationToken-1.0">
|
||||
abc123
|
||||
</allocationToken:allocationToken>
|
||||
</extension>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
</command>
|
||||
</epp>
|
||||
@@ -9,9 +9,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -20,9 +20,6 @@
|
||||
<domain:hostObj>ns12.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns13.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -21,9 +21,6 @@
|
||||
<domain:hostObj>ns13.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns14.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -10,9 +10,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -10,9 +10,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -10,9 +10,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -10,9 +10,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -10,9 +10,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -5,9 +5,6 @@
|
||||
<domain:create
|
||||
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
|
||||
<domain:name>exampleone.tld</domain:name>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -11,9 +11,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -11,9 +11,6 @@
|
||||
<domain:hostObj>ns1.example.tld</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.tld</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -11,9 +11,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -11,9 +11,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -11,9 +11,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -11,9 +11,6 @@
|
||||
<domain:hostObj>ns1.example.tld</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.tld</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -1,23 +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:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="admin">jd1234</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
</domain:create>
|
||||
</create>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
</command>
|
||||
</epp>
|
||||
@@ -4,9 +4,6 @@
|
||||
<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:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -1,21 +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:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
</domain:create>
|
||||
</create>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
</command>
|
||||
</epp>
|
||||
@@ -9,9 +9,7 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact>sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -8,9 +8,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -1,21 +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:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
</domain:create>
|
||||
</create>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
</command>
|
||||
</epp>
|
||||
@@ -9,9 +9,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -1,19 +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:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
</domain:create>
|
||||
</create>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
</command>
|
||||
</epp>
|
||||
@@ -5,9 +5,6 @@
|
||||
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
|
||||
<domain:name>%DOMAIN%</domain:name>
|
||||
<domain:period unit="y">2</domain:period>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -10,9 +10,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -10,9 +10,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -10,9 +10,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -10,9 +10,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<domain:hostObj>ns1.example.net</domain:hostObj>
|
||||
<domain:hostObj>ns2.example.net</domain:hostObj>
|
||||
</domain:ns>
|
||||
<domain:registrant>jd1234</domain:registrant>
|
||||
<domain:contact type="admin">sh8013</domain:contact>
|
||||
<domain:contact type="tech">sh8013</domain:contact>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR</domain:pw>
|
||||
</domain:authInfo>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user