Compare commits

..
Author SHA1 Message Date
sarahcaseybotandGitHub 74f0a8dd7b Add nomulus tool command for FeatureFlags (#2480)
* Add registryTool commands for FeatureFlags

* Fix merge conflicts

* Add required parameters and inject mapper

* Use optionals in cache to negative cahe missing objects

* Fix spelling

* Change back to bulk load in cache

* Add FeatureName enum

* Change variable name

* Use FeatureName in main parameter
2024-07-09 20:05:15 +00:00
gbrodmanandGitHub 092e3dca47 Add a renewal cost for ATs when renewal is SPECIFIED (#2484)
Note: this is not used yet
2024-07-09 18:39:48 +00:00
gbrodmanandGitHub b8a6ac72dd Add a reg-lock verification action to the new console (#2467)
The front end will have a (hidden) page that passes the verification
code to this API endpoint and displays the result.
2024-07-08 21:25:22 +00:00
Ben McIlwainandGitHub b602aac09a Make all contacts nullable in the data model (#2490)
This doesn't yet allow them to be absent in EPP flows, but it should make the
code not break if they happen to be null in the database. This is a follow-up to
PR #2477, which ends up being a bit easier because whereas the registrant is
used in more parts of the codebase, the other contact types (admin, technical,
billing) are really only used in RDE, WHOIS, and RDAP, and because they were
already being used as a collection anyway, the handling for if that collection
contains fewer elements or is empty happened to already be mostly correct.
2024-07-03 21:42:20 +00:00
Lai JiangandGitHub d86c002132 Create Users when setting up OT&E and Production registrars (#2488) 2024-07-03 18:31:07 +00:00
gbrodmanandGitHub 54c5a9450d Add RegistrationBehavior.NONPREMIUM_CREATE (#2481)
When using this token (which must be tied to a particular domain), the
first year price (and only the first year price, i.e. the creation
price) will be the standard price for this TLD. Future years (i.e.
renewals) will continue at the normal premium price.
2024-06-26 20:01:32 +00:00
gbrodmanandGitHub 0f0097c15c Wait to load domain list until a registrar is selected (#2485)
This isn't the worst thing in the world but it does result in a bad
request to the server otherwise, and log/error spam. So, only load the
domains list if we have a registrar selected.
2024-06-25 18:39:53 +00:00
Ben McIlwainandGitHub c9437d8c72 Make registrant nullable on domains (#2477)
This is the first step in migrating to the minimum registration data set. Note
that our database model already permits null domain registrants, so this just
makes the code accept it as well. Note that I haven't changed any requirements
in EPP flows yet; a later step will be to check the migration schedule and then
not require the registrant to be present if in a suitable state.

This does potentially affect the output of WHOIS/RDAP, but that's a NOOP so long
as EPP commands and other tools continue to enforce the requirement of a
registrant.
2024-06-20 15:22:38 +00:00
107 changed files with 6525 additions and 4131 deletions
@@ -60,8 +60,9 @@ export class DomainListComponent {
effect(() => {
this.pageNumber = 0;
this.totalResults = 0;
this.reloadData();
this.registrarService.registrarId();
if (this.registrarService.registrarId()) {
this.reloadData();
}
});
}
@@ -466,13 +466,10 @@ public class RdePipeline implements Serializable {
// Contacts and hosts are only deposited in RDE, not BRDA.
if (pendingDeposit.mode() == RdeMode.FULL) {
HashSet<Serializable> contacts = new HashSet<>();
contacts.add(domain.getAdminContact().getKey());
contacts.add(domain.getTechContact().getKey());
contacts.add(domain.getRegistrant().getKey());
// Billing contact is not mandatory.
if (domain.getBillingContact() != null) {
contacts.add(domain.getBillingContact().getKey());
}
domain.getAdminContact().ifPresent(c -> contacts.add(c.getKey()));
domain.getTechContact().ifPresent(c -> contacts.add(c.getKey()));
domain.getRegistrant().ifPresent(c -> contacts.add(c.getKey()));
domain.getBillingContact().ifPresent(c -> contacts.add(c.getKey()));
referencedContactCounter.inc(contacts.size());
contacts.forEach(
contactRepoId ->
@@ -96,6 +96,14 @@
<max-retry-duration>3600s</max-retry-duration>
</queue>
<!-- Queue for tasks that update membership in the console user group. -->
<queue>
<name>console-user-group-update</name>
<max-dispatches-per-second>1</max-dispatches-per-second>
<max-concurrent-dispatches>1</max-concurrent-dispatches>
<max-retry-duration>3600s</max-retry-duration>
</queue>
<!-- Queue for infrequent cron tasks (i.e. hourly or less often) that should retry three times on failure. -->
<queue>
<name>retryable-cron-tasks</name>
@@ -412,11 +412,13 @@ public class DomainFlowUtils {
/** Verify that no linked resources have disallowed statuses. */
static void verifyNotInPendingDelete(
Set<DesignatedContact> contacts, VKey<Contact> registrant, Set<VKey<Host>> nameservers)
Set<DesignatedContact> contacts,
Optional<VKey<Contact>> registrant,
Set<VKey<Host>> nameservers)
throws EppException {
ImmutableList.Builder<VKey<? extends EppResource>> keysToLoad = new ImmutableList.Builder<>();
contacts.stream().map(DesignatedContact::getContactKey).forEach(keysToLoad::add);
Optional.ofNullable(registrant).ifPresent(keysToLoad::add);
registrant.ifPresent(keysToLoad::add);
keysToLoad.addAll(nameservers);
verifyNotInPendingDelete(EppResource.loadCached(keysToLoad.build()).values());
}
@@ -480,9 +482,10 @@ public class DomainFlowUtils {
}
static void validateRequiredContactsPresent(
@Nullable VKey<Contact> registrant, Set<DesignatedContact> contacts)
Optional<VKey<Contact>> registrant, Set<DesignatedContact> contacts)
throws RequiredParameterMissingException {
if (registrant == null) {
// TODO: Check minimum reg data set migration schedule here and don't throw when any are empty.
if (registrant.isEmpty()) {
throw new MissingRegistrantException();
}
@@ -498,14 +501,14 @@ public class DomainFlowUtils {
}
}
static void validateRegistrantAllowedOnTld(String tld, String registrantContactId)
static void validateRegistrantAllowedOnTld(String tld, Optional<String> registrantContactId)
throws RegistrantNotAllowedException {
ImmutableSet<String> allowedRegistrants = Tld.get(tld).getAllowedRegistrantContactIds();
// Empty allow list or null registrantContactId are ignored.
if (registrantContactId != null
if (registrantContactId.isPresent()
&& !allowedRegistrants.isEmpty()
&& !allowedRegistrants.contains(registrantContactId)) {
throw new RegistrantNotAllowedException(registrantContactId);
&& !allowedRegistrants.contains(registrantContactId.get())) {
throw new RegistrantNotAllowedException(registrantContactId.get());
}
}
@@ -119,8 +119,11 @@ public final class DomainInfoFlow implements TransactionalFlow {
.setCreationTime(domain.getCreationTime())
.setLastEppUpdateTime(domain.getLastEppUpdateTime())
.setRegistrationExpirationTime(domain.getRegistrationExpirationTime())
.setLastTransferTime(domain.getLastTransferTime())
.setRegistrant(tm().loadByKey(domain.getRegistrant()).getContactId());
.setLastTransferTime(domain.getLastTransferTime());
domain
.getRegistrant()
.ifPresent(r -> infoBuilder.setRegistrant(tm().loadByKey(r).getContactId()));
// If authInfo is non-null, then the caller is authorized to see the full information since we
// will have already verified the authInfo is valid.
if (registrarId.equals(domain.getCurrentSponsorRegistrarId()) || authInfo.isPresent()) {
@@ -85,6 +85,15 @@ public final class DomainPricingLogic {
createFee = Fee.create(zeroInCurrency(currency), FeeType.CREATE, false);
} else {
DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime);
if (allocationToken.isPresent()
&& allocationToken
.get()
.getRegistrationBehavior()
.equals(RegistrationBehavior.NONPREMIUM_CREATE)) {
domainPrices =
DomainPrices.create(
false, tld.getCreateBillingCost(dateTime), domainPrices.getRenewCost());
}
Money domainCreateCost =
getDomainCreateCostWithDiscount(domainPrices, years, allocationToken);
// Apply a sunrise discount if configured and applicable
@@ -278,7 +278,7 @@ public final class DomainUpdateFlow implements MutatingFlow {
.removeStatusValues(remove.getStatusValues())
.removeContacts(remove.getContacts())
.addContacts(add.getContacts())
.setRegistrant(firstNonNull(change.getRegistrant(), domain.getRegistrant()))
.setRegistrant(change.getRegistrant().or(domain::getRegistrant))
.setAuthInfo(firstNonNull(change.getAuthInfo(), domain.getAuthInfo()));
if (!add.getNameservers().isEmpty()) {
@@ -301,7 +301,10 @@ public final class DomainUpdateFlow implements MutatingFlow {
}
private static void validateRegistrantIsntBeingRemoved(Change change) throws EppException {
if (change.getRegistrantContactId() != null && change.getRegistrantContactId().isEmpty()) {
// TODO(mcilwain): Make this check the minimum registration data set migration schedule
// and not require presence of a registrant in later stages.
if (change.getRegistrantContactId().isPresent()
&& change.getRegistrantContactId().get().isEmpty()) {
throw new MissingRegistrantException();
}
}
@@ -31,6 +31,7 @@ import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature;
import com.google.common.collect.ImmutableSortedSet;
import google.registry.model.common.FeatureFlag.FeatureStatus;
import google.registry.model.common.TimedTransitionProperty;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.tld.Tld.TldState;
@@ -363,6 +364,33 @@ public class EntityYamlUtils {
}
}
/** A custom JSON deserializer for a {@link TimedTransitionProperty} of {@link FeatureStatus}. */
public static class TimedTransitionPropertyFeatureStatusDeserializer
extends StdDeserializer<TimedTransitionProperty<FeatureStatus>> {
public TimedTransitionPropertyFeatureStatusDeserializer() {
this(null);
}
public TimedTransitionPropertyFeatureStatusDeserializer(
Class<TimedTransitionProperty<FeatureStatus>> t) {
super(t);
}
@Override
public TimedTransitionProperty<FeatureStatus> deserialize(
JsonParser jp, DeserializationContext context) throws IOException {
SortedMap<String, String> valueMap = jp.readValueAs(SortedMap.class);
return TimedTransitionProperty.fromValueMap(
valueMap.keySet().stream()
.collect(
toImmutableSortedMap(
natural(),
DateTime::parse,
key -> FeatureStatus.valueOf(valueMap.get(key)))));
}
}
/** A custom JSON deserializer for a {@link CreateAutoTimestamp}. */
public static class CreateAutoTimestampDeserializer extends StdDeserializer<CreateAutoTimestamp> {
@@ -17,6 +17,7 @@ package google.registry.model;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static google.registry.model.tld.Tld.TldState.GENERAL_AVAILABILITY;
import static google.registry.model.tld.Tld.TldState.START_DATE_SUNRISE;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
@@ -28,19 +29,28 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import google.registry.batch.CloudTasksUtils;
import google.registry.model.console.RegistrarRole;
import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import google.registry.model.console.UserRoles;
import google.registry.model.pricing.StaticPremiumListPricingEngine;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarAddress;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.model.tld.Tld;
import google.registry.model.tld.Tld.TldState;
import google.registry.model.tld.Tld.TldType;
import google.registry.model.tld.label.PremiumList;
import google.registry.model.tld.label.PremiumListDao;
import google.registry.persistence.VKey;
import google.registry.tools.IamClient;
import google.registry.util.CidrAddressBlock;
import google.registry.util.RegistryEnvironment;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.regex.Pattern;
@@ -75,8 +85,8 @@ public final class OteAccountBuilder {
* Validation regex for registrar base client IDs (3-14 lowercase alphanumeric characters).
*
* <p>The base client ID is appended with numbers to create four different test registrar accounts
* (e.g. reg-1, reg-3, reg-4, reg-5). Registrar client IDs are of type clIDType in eppcom.xsd
* which is limited to 16 characters, hence the limit of 14 here to account for the dash and
* (e.g., reg-1, reg-3, reg-4, reg-5). Registrar client IDs are of type clIDType in eppcom.xsd
* that is limited to 16 characters, hence the limit of 14 here to account for the dash and
* numbers.
*
* <p>The base client ID is also used to generate the OT&E TLDs, hence the restriction to
@@ -113,7 +123,7 @@ public final class OteAccountBuilder {
* The default billing account map applied to all OT&amp;E registrars.
*
* <p>This contains dummy values for USD and JPY so that OT&amp;E registrars can be granted access
* to all existing TLDs in sandbox. Note that OT&amp;E is only on sandbox and thus these dummy
* to all existing TLDs in sandbox. Note that OT&amp;E is only on sandbox, and thus these dummy
* values will never be used in production (the only environment where real invoicing takes
* place).
*/
@@ -124,7 +134,7 @@ public final class OteAccountBuilder {
private final Tld sunriseTld;
private final Tld gaTld;
private final Tld eapTld;
private final ImmutableList.Builder<RegistrarPoc> contactsBuilder = new ImmutableList.Builder<>();
private final List<User> users = new ArrayList<>();
private ImmutableList<Registrar> registrars;
private boolean replaceExisting = false;
@@ -172,16 +182,28 @@ public final class OteAccountBuilder {
}
/**
* Adds a RegistrarContact with Web Console access.
* Adds a {@link User} with Web Console access.
*
* <p>NOTE: can be called more than once, adding multiple contacts. Each contact will have access
* to all OT&amp;E Registrars.
* <p>NOTE: can be called more than once, adding multiple users. Each user will have access to all
* OT&amp;E Registrars.
*
* @param email the contact/login email that will have web-console access to all the Registrars.
* Must be from "our G Suite domain".
* @param email the login email that will have web-console access to all the Registrars. Must be
* from "our Google Workspace domain".
*/
public OteAccountBuilder addContact(String email) {
registrars.forEach(registrar -> contactsBuilder.add(createRegistrarContact(email, registrar)));
public OteAccountBuilder addUser(String email) {
users.add(
new User.Builder()
.setEmailAddress(email)
.setUserRoles(
new UserRoles.Builder()
.setRegistrarRoles(
registrars.stream()
.collect(
toImmutableMap(
Registrar::getRegistrarId,
registrar -> RegistrarRole.ACCOUNT_MANAGER)))
.build())
.build());
return this;
}
@@ -217,7 +239,7 @@ public final class OteAccountBuilder {
return transformRegistrars(builder -> builder.setClientCertificate(asciiCert, now));
}
/** Sets the IP allow list to all the OT&amp;E Registrars. */
/** Sets the IP allowlist to all the OT&amp;E Registrars. */
public OteAccountBuilder setIpAllowList(Collection<String> ipAllowList) {
ImmutableList<CidrAddressBlock> ipAddressAllowList =
ipAllowList.stream().map(CidrAddressBlock::create).collect(toImmutableList());
@@ -237,18 +259,37 @@ public final class OteAccountBuilder {
}
/**
* Return map from the OT&amp;E registrarIds we will create to the new TLDs they will have access
* to.
* Return the map from the OT&amp;E registrarIds we will create to the new TLDs they will have
* access to.
*/
public ImmutableMap<String, String> getRegistrarIdToTldMap() {
return registrarIdToTld;
}
/** Grants the users permission to pass IAP. */
public void grantIapPermission(
Optional<String> groupEmailAddress, CloudTasksUtils cloudTasksUtils, IamClient iamClient) {
for (User user : users) {
User.grantIapPermission(
user.getEmailAddress(), groupEmailAddress, cloudTasksUtils, iamClient);
}
}
/** Saves all the OT&amp;E entities we created. */
private void saveAllEntities() {
// use ImmutableObject instead of Registry so that the Key generation doesn't break
ImmutableList<Tld> registries = ImmutableList.of(sunriseTld, gaTld, eapTld);
ImmutableList<RegistrarPoc> contacts = contactsBuilder.build();
Map<String, User> existingUsers = new HashMap<>();
users.forEach(
user ->
UserDao.loadUser(user.getEmailAddress())
.ifPresent(
existingUser ->
existingUsers.put(existingUser.getEmailAddress(), existingUser)));
if (!replaceExisting) {
checkState(existingUsers.isEmpty(), "Found existing users: %s", existingUsers);
}
tm().transact(
() -> {
@@ -256,8 +297,7 @@ public final class OteAccountBuilder {
ImmutableList<VKey<? extends ImmutableObject>> keys =
Streams.concat(
registries.stream().map(tld -> Tld.createVKey(tld.getTldStr())),
registrars.stream().map(Registrar::createVKey),
contacts.stream().map(RegistrarPoc::createVKey))
registrars.stream().map(Registrar::createVKey))
.collect(toImmutableList());
ImmutableMap<VKey<? extends ImmutableObject>, ImmutableObject> existingObjects =
tm().loadByKeysIfPresent(keys);
@@ -275,8 +315,18 @@ public final class OteAccountBuilder {
registrars = registrars.stream().map(this::addAllowedTld).collect(toImmutableList());
// and we can save the registrars and contacts!
tm().putAll(registrars);
tm().putAll(contacts);
});
for (User user : users) {
String email = user.getEmailAddress();
if (existingUsers.containsKey(email)) {
// Note that other roles for the existing user are reset. We do this instead of simply
// saving the new user is that UserDao does not allow us to save the new user with the same
// email as the existing user.
user = existingUsers.get(email).asBuilder().setUserRoles(user.getUserRoles()).build();
}
UserDao.saveUser(user);
}
}
private Registrar addAllowedTld(Registrar registrar) {
@@ -336,15 +386,6 @@ public final class OteAccountBuilder {
.build();
}
private static RegistrarPoc createRegistrarContact(String email, Registrar registrar) {
return new RegistrarPoc.Builder()
.setRegistrar(registrar)
.setName(email)
.setEmailAddress(email)
.setLoginEmailAddress(email)
.build();
}
/** Returns the registrar IDs of the OT&amp;E, with the TLDs each has access to. */
public static ImmutableMap<String, String> createRegistrarIdToTldMap(String baseRegistrarId) {
checkArgument(
@@ -14,29 +14,34 @@
package google.registry.model.common;
import static com.google.api.client.util.Preconditions.checkState;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.config.RegistryConfig.getSingletonCacheRefreshDuration;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Sets;
import com.google.common.collect.Maps;
import google.registry.model.Buildable;
import google.registry.model.CacheUtils;
import google.registry.model.EntityYamlUtils.TimedTransitionPropertyFeatureStatusDeserializer;
import google.registry.model.ImmutableObject;
import google.registry.persistence.VKey;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.Id;
import org.joda.time.DateTime;
@@ -54,60 +59,76 @@ public class FeatureFlag extends ImmutableObject implements Buildable {
INACTIVE
}
public enum FeatureName {
TEST_FEATURE,
MINIMUM_DATASET_CONTACTS_OPTIONAL,
MINIMUM_DATASET_CONTACTS_PROHIBITED
}
/** The name of the flag/feature. */
@Id String featureName;
@Enumerated(EnumType.STRING)
@Id
FeatureName featureName;
/** A map of times for each {@link FeatureStatus} the FeatureFlag should hold. */
@Column(nullable = false)
@JsonDeserialize(using = TimedTransitionPropertyFeatureStatusDeserializer.class)
TimedTransitionProperty<FeatureStatus> status =
TimedTransitionProperty.withInitialValue(FeatureStatus.INACTIVE);
public static FeatureFlag get(String featureName) {
FeatureFlag maybeFeatureFlag = CACHE.get(featureName);
if (maybeFeatureFlag == null) {
throw new FeatureFlagNotFoundException(featureName);
} else {
return maybeFeatureFlag;
}
public static Optional<FeatureFlag> getUncached(FeatureName featureName) {
return tm().transact(() -> tm().loadByKeyIfPresent(createVKey(featureName)));
}
public static ImmutableSet<FeatureFlag> get(Set<String> featureNames) {
Map<String, FeatureFlag> featureFlags = CACHE.getAll(featureNames);
ImmutableSet<String> missingFlags =
Sets.difference(featureNames, featureFlags.keySet()).immutableCopy();
public static ImmutableList<FeatureFlag> getAllUncached() {
return tm().transact(() -> tm().loadAllOf(FeatureFlag.class));
}
public static FeatureFlag get(FeatureName featureName) {
Optional<FeatureFlag> maybeFeatureFlag = CACHE.get(featureName);
return maybeFeatureFlag.orElseThrow(() -> new FeatureFlagNotFoundException(featureName));
}
public static ImmutableSet<FeatureFlag> getAll(Set<FeatureName> featureNames) {
Map<FeatureName, Optional<FeatureFlag>> featureFlags = CACHE.getAll(featureNames);
ImmutableSet<FeatureName> missingFlags =
featureFlags.entrySet().stream()
.filter(e -> e.getValue().isEmpty())
.map(Map.Entry::getKey)
.collect(toImmutableSet());
if (missingFlags.isEmpty()) {
return featureFlags.values().stream().collect(toImmutableSet());
return featureFlags.values().stream().map(Optional::get).collect(toImmutableSet());
} else {
throw new FeatureFlagNotFoundException(missingFlags);
}
}
/** A cache that loads the {@link FeatureFlag} for a given featureName. */
private static final LoadingCache<String, FeatureFlag> CACHE =
private static final LoadingCache<FeatureName, Optional<FeatureFlag>> CACHE =
CacheUtils.newCacheBuilder(getSingletonCacheRefreshDuration())
.build(
new CacheLoader<>() {
@Override
public FeatureFlag load(final String featureName) {
return tm().reTransact(() -> tm().loadByKeyIfPresent(createVKey(featureName)))
.orElse(null);
public Optional<FeatureFlag> load(final FeatureName featureName) {
return tm().reTransact(() -> tm().loadByKeyIfPresent(createVKey(featureName)));
}
@Override
public Map<? extends String, ? extends FeatureFlag> loadAll(
Set<? extends String> featureFlagNames) {
ImmutableMap<String, VKey<FeatureFlag>> keysMap =
public Map<? extends FeatureName, ? extends Optional<FeatureFlag>> loadAll(
Set<? extends FeatureName> featureFlagNames) {
ImmutableMap<FeatureName, VKey<FeatureFlag>> keysMap =
featureFlagNames.stream()
.collect(
toImmutableMap(featureName -> featureName, FeatureFlag::createVKey));
Map<VKey<? extends FeatureFlag>, FeatureFlag> entities =
tm().reTransact(() -> tm().loadByKeysIfPresent(keysMap.values()));
return entities.values().stream()
.collect(toImmutableMap(flag -> flag.featureName, flag -> flag));
return Maps.toMap(
featureFlagNames,
name -> Optional.ofNullable(entities.get(createVKey(name))));
}
});
public static VKey<FeatureFlag> createVKey(String featureName) {
public static VKey<FeatureFlag> createVKey(FeatureName featureName) {
return VKey.create(FeatureFlag.class, featureName);
}
@@ -116,10 +137,11 @@ public class FeatureFlag extends ImmutableObject implements Buildable {
return createVKey(featureName);
}
public String getFeatureName() {
public FeatureName getFeatureName() {
return featureName;
}
@JsonProperty("status")
public TimedTransitionProperty<FeatureStatus> getStatusMap() {
return status;
}
@@ -144,20 +166,17 @@ public class FeatureFlag extends ImmutableObject implements Buildable {
@Override
public FeatureFlag build() {
checkArgument(
!Strings.isNullOrEmpty(getInstance().featureName),
"Feature name must not be null or empty");
getInstance().status.checkValidity();
checkArgument(getInstance().featureName != null, "FeatureName cannot be null");
return super.build();
}
public Builder setFeatureName(String featureName) {
checkState(getInstance().featureName == null, "Feature name can only be set once");
public Builder setFeatureName(FeatureName featureName) {
getInstance().featureName = featureName;
return this;
}
public Builder setStatus(ImmutableSortedMap<DateTime, FeatureStatus> statusMap) {
public Builder setStatusMap(ImmutableSortedMap<DateTime, FeatureStatus> statusMap) {
getInstance().status = TimedTransitionProperty.fromValueMap(statusMap);
return this;
}
@@ -166,11 +185,11 @@ public class FeatureFlag extends ImmutableObject implements Buildable {
/** Exception to throw when no FeatureFlag entity is found for given FeatureName string(s). */
public static class FeatureFlagNotFoundException extends RuntimeException {
FeatureFlagNotFoundException(ImmutableSet<String> featureNames) {
FeatureFlagNotFoundException(ImmutableSet<FeatureName> featureNames) {
super("No feature flag object(s) found for " + Joiner.on(", ").join(featureNames));
}
FeatureFlagNotFoundException(String featureName) {
public FeatureFlagNotFoundException(FeatureName featureName) {
this(ImmutableSet.of(featureName));
}
}
@@ -14,7 +14,19 @@
package google.registry.model.console;
import static google.registry.tools.server.UpdateUserGroupAction.GROUP_UPDATE_QUEUE;
import com.google.cloud.tasks.v2.Task;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.flogger.FluentLogger;
import google.registry.batch.CloudTasksUtils;
import google.registry.persistence.VKey;
import google.registry.request.Action.Service;
import google.registry.tools.IamClient;
import google.registry.tools.server.UpdateUserGroupAction;
import google.registry.tools.server.UpdateUserGroupAction.Mode;
import google.registry.util.RegistryEnvironment;
import java.util.Optional;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Embeddable;
@@ -31,6 +43,76 @@ import javax.persistence.Table;
@Table(indexes = {@Index(columnList = "emailAddress", name = "user_email_address_idx")})
public class User extends UserBase {
public static final String IAP_SECURED_WEB_APP_USER_ROLE = "roles/iap.httpsResourceAccessor";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/**
* Grants the user permission to pass IAP.
*
* <p>Depending on if a console user group is set up, the permission is granted either
* individually or via group membership.
*/
public static void grantIapPermission(
String emailAddress,
Optional<String> groupEmailAddress,
CloudTasksUtils cloudTasksUtils,
IamClient iamClient) {
if (RegistryEnvironment.isInTestServer()) {
return;
}
if (groupEmailAddress.isEmpty()) {
logger.atInfo().log("Granting IAP role to user %s", emailAddress);
iamClient.addBinding(emailAddress, IAP_SECURED_WEB_APP_USER_ROLE);
} else {
logger.atInfo().log("Adding %s to group %s", emailAddress, groupEmailAddress.get());
modifyGroupMembershipAsync(
emailAddress, groupEmailAddress.get(), cloudTasksUtils, UpdateUserGroupAction.Mode.ADD);
}
}
/**
* Revoke the user's permission to pass IAP.
*
* <p>Depending on if a console user group is set up, the permission is revoked either
* individually or via group membership.
*/
public static void revokeIapPermission(
String emailAddress,
Optional<String> groupEmailAddress,
CloudTasksUtils cloudTasksUtils,
IamClient iamClient) {
if (RegistryEnvironment.isInTestServer()) {
return;
}
if (groupEmailAddress.isEmpty()) {
logger.atInfo().log("Removing IAP role from user %s", emailAddress);
iamClient.removeBinding(emailAddress, IAP_SECURED_WEB_APP_USER_ROLE);
} else {
logger.atInfo().log("Removing %s from group %s", emailAddress, groupEmailAddress.get());
modifyGroupMembershipAsync(
emailAddress, groupEmailAddress.get(), cloudTasksUtils, Mode.REMOVE);
}
}
private static void modifyGroupMembershipAsync(
String userEmailAddress,
String groupEmailAddress,
CloudTasksUtils cloudTasksUtils,
Mode mode) {
Task task =
cloudTasksUtils.createPostTask(
UpdateUserGroupAction.PATH,
Service.TOOLS,
ImmutableMultimap.of(
"userEmailAddress",
userEmailAddress,
"groupEmailAddress",
groupEmailAddress,
"groupUpdateMode",
mode.name()));
cloudTasksUtils.enqueue(GROUP_UPDATE_QUEUE, task);
}
@Override
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -46,6 +46,7 @@ import google.registry.model.EppResource;
import google.registry.model.EppResource.ResourceWithTransferData;
import google.registry.model.billing.BillingRecurrence;
import google.registry.model.contact.Contact;
import google.registry.model.domain.DesignatedContact.Type;
import google.registry.model.domain.launch.LaunchNotice;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.domain.secdns.DomainDsData;
@@ -131,11 +132,11 @@ public class DomainBase extends EppResource
@Expose @Transient Set<VKey<Host>> nsHosts;
/** Contacts. */
@Expose VKey<Contact> adminContact;
@Expose @Nullable VKey<Contact> adminContact;
@Expose VKey<Contact> billingContact;
@Expose VKey<Contact> techContact;
@Expose VKey<Contact> registrantContact;
@Expose @Nullable VKey<Contact> billingContact;
@Expose @Nullable VKey<Contact> techContact;
@Expose @Nullable VKey<Contact> registrantContact;
/** Authorization info (aka transfer secret) of the domain. */
@Embedded
@@ -585,32 +586,49 @@ public class DomainBase extends EppResource
}
/** A key to the registrant who registered this domain. */
public VKey<Contact> getRegistrant() {
return registrantContact;
public Optional<VKey<Contact>> getRegistrant() {
return Optional.ofNullable(registrantContact);
}
public VKey<Contact> getAdminContact() {
return adminContact;
public Optional<VKey<Contact>> getAdminContact() {
return Optional.ofNullable(adminContact);
}
public VKey<Contact> getBillingContact() {
return billingContact;
public Optional<VKey<Contact>> getBillingContact() {
return Optional.ofNullable(billingContact);
}
public VKey<Contact> getTechContact() {
return techContact;
public Optional<VKey<Contact>> getTechContact() {
return Optional.ofNullable(techContact);
}
/** Associated contacts for the domain (other than registrant). */
/**
* Associated contacts for the domain (other than registrant).
*
* <p>Note: This can be an empty set if no contacts are present for the domain.
*/
public ImmutableSet<DesignatedContact> getContacts() {
return getAllContacts(false);
}
/**
* Gets all associated contacts for the domain, including the registrant.
*
* <p>Note: This can be an empty set if no contacts are present for the domain.
*/
public ImmutableSet<DesignatedContact> getAllContacts() {
return getAllContacts(true);
}
public DomainAuthInfo getAuthInfo() {
return authInfo;
}
/** Returns all referenced contacts from this domain. */
/**
* Returns all referenced contacts from this domain.
*
* <p>Note: This can be an empty set if no contacts are present for the domain.
*/
public ImmutableSet<VKey<Contact>> getReferencedContacts() {
return nullToEmptyImmutableCopy(getAllContacts(true)).stream()
.map(DesignatedContact::getContactKey)
@@ -620,18 +638,12 @@ public class DomainBase extends EppResource
private ImmutableSet<DesignatedContact> getAllContacts(boolean includeRegistrant) {
ImmutableSet.Builder<DesignatedContact> builder = new ImmutableSet.Builder<>();
if (includeRegistrant && registrantContact != null) {
builder.add(DesignatedContact.create(DesignatedContact.Type.REGISTRANT, registrantContact));
}
if (adminContact != null) {
builder.add(DesignatedContact.create(DesignatedContact.Type.ADMIN, adminContact));
}
if (billingContact != null) {
builder.add(DesignatedContact.create(DesignatedContact.Type.BILLING, billingContact));
}
if (techContact != null) {
builder.add(DesignatedContact.create(DesignatedContact.Type.TECH, techContact));
if (includeRegistrant) {
getRegistrant().ifPresent(c -> builder.add(DesignatedContact.create(Type.REGISTRANT, c)));
}
getAdminContact().ifPresent(c -> builder.add(DesignatedContact.create(Type.ADMIN, c)));
getBillingContact().ifPresent(c -> builder.add(DesignatedContact.create(Type.BILLING, c)));
getTechContact().ifPresent(c -> builder.add(DesignatedContact.create(Type.TECH, c)));
return builder.build();
}
@@ -647,11 +659,13 @@ public class DomainBase extends EppResource
*/
void setContactFields(Set<DesignatedContact> contacts, boolean includeRegistrant) {
// Set the individual contact fields.
billingContact = techContact = adminContact = null;
billingContact = null;
techContact = null;
adminContact = null;
if (includeRegistrant) {
registrantContact = null;
}
HashSet<DesignatedContact.Type> contactsDiscovered = new HashSet<>();
HashSet<Type> contactsDiscovered = new HashSet<>();
for (DesignatedContact contact : contacts) {
checkArgument(
!contactsDiscovered.contains(contact.getType()),
@@ -682,7 +696,7 @@ public class DomainBase extends EppResource
/** Predicate to determine if a given {@link DesignatedContact} is the registrant. */
static final Predicate<DesignatedContact> IS_REGISTRANT =
(DesignatedContact contact) -> DesignatedContact.Type.REGISTRANT.equals(contact.type);
(DesignatedContact contact) -> Type.REGISTRANT.equals(contact.type);
/** An override of {@link EppResource#asBuilder} with tighter typing. */
@Override
@@ -717,7 +731,6 @@ public class DomainBase extends EppResource
instance.autorenewEndTime = firstNonNull(getInstance().autorenewEndTime, END_OF_TIME);
checkArgumentNotNull(emptyToNull(instance.domainName), "Missing domainName");
checkArgumentNotNull(instance.getRegistrant(), "Missing registrant");
instance.tld = getTldFromDomainName(instance.domainName);
T newDomain = super.build();
@@ -749,9 +762,9 @@ public class DomainBase extends EppResource
return thisCastToDerived();
}
public B setRegistrant(VKey<Contact> registrant) {
public B setRegistrant(Optional<VKey<Contact>> registrant) {
// Set the registrant field specifically.
getInstance().registrantContact = registrant;
getInstance().registrantContact = registrant.orElse(null);
return thisCastToDerived();
}
@@ -40,6 +40,7 @@ import google.registry.model.eppinput.ResourceCommand.ResourceUpdate;
import google.registry.model.eppinput.ResourceCommand.SingleResourceCommand;
import google.registry.model.host.Host;
import google.registry.persistence.VKey;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nullable;
import javax.xml.bind.annotation.XmlAttribute;
@@ -76,21 +77,21 @@ public class DomainCommand {
/** The contactId of the registrant who registered this domain. */
@XmlElement(name = "registrant")
@Nullable
String registrantContactId;
/** A resolved key to the registrant who registered this domain. */
@XmlTransient VKey<Contact> registrant;
@Nullable @XmlTransient VKey<Contact> registrant;
/** Authorization info (aka transfer secret) of the domain. */
DomainAuthInfo authInfo;
public String getRegistrantContactId() {
return registrantContactId;
public Optional<String> getRegistrantContactId() {
return Optional.ofNullable(registrantContactId);
}
@Nullable
public VKey<Contact> getRegistrant() {
return registrant;
public Optional<VKey<Contact>> getRegistrant() {
return Optional.ofNullable(registrant);
}
public DomainAuthInfo getAuthInfo() {
@@ -63,6 +63,7 @@ public abstract class DomainInfoData implements ResponseData {
abstract ImmutableSet<StatusValue> getStatusValues();
@XmlElement(name = "registrant")
@Nullable
abstract String getRegistrant();
@XmlElement(name = "contact")
@@ -50,6 +50,7 @@ import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName
import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
import google.registry.persistence.VKey;
import google.registry.persistence.WithVKey;
import google.registry.persistence.converter.JodaMoneyType;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -63,6 +64,9 @@ import javax.persistence.Enumerated;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.Table;
import org.hibernate.annotations.Columns;
import org.hibernate.annotations.Type;
import org.joda.money.Money;
import org.joda.time.DateTime;
/** An entity representing an allocation token. */
@@ -115,7 +119,17 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda
*/
BYPASS_TLD_STATE,
/** Bypasses most checks and creates the domain as an anchor tenant, with all that implies. */
ANCHOR_TENANT
ANCHOR_TENANT,
/**
* Bypasses the premium list to use the standard creation price. Does not affect the renewal
* price.
*
* <p>This cannot be specified along with a discount fraction, and any renewals (automatic or
* otherwise) will use the premium price for the domain if one exists.
*
* <p>Tokens with this behavior must be tied to a single particular domain.
*/
NONPREMIUM_CREATE
}
/** Type of the token that indicates how and where it should be used. */
@@ -226,6 +240,12 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda
@Column(name = "renewalPriceBehavior", nullable = false)
RenewalPriceBehavior renewalPriceBehavior = RenewalPriceBehavior.DEFAULT;
/** The price used for renewals iff the renewalPriceBehavior is SPECIFIED. */
@Nullable
@Type(type = JodaMoneyType.TYPE_NAME)
@Columns(columns = {@Column(name = "renewalPriceAmount"), @Column(name = "renewalPriceCurrency")})
Money renewalPrice;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
RegistrationBehavior registrationBehavior = RegistrationBehavior.DEFAULT;
@@ -300,6 +320,10 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda
return renewalPriceBehavior;
}
public Optional<Money> getRenewalPrice() {
return Optional.ofNullable(renewalPrice);
}
public RegistrationBehavior getRegistrationBehavior() {
return registrationBehavior;
}
@@ -367,29 +391,12 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda
public AllocationToken build() {
checkArgumentNotNull(getInstance().tokenType, "Token type must be specified");
checkArgument(!Strings.isNullOrEmpty(getInstance().token), "Token must not be null or empty");
checkArgument(
!getInstance().tokenType.equals(TokenType.BULK_PRICING)
|| getInstance().renewalPriceBehavior.equals(RenewalPriceBehavior.SPECIFIED),
"Bulk tokens must have renewalPriceBehavior set to SPECIFIED");
checkArgument(
!getInstance().tokenType.equals(TokenType.BULK_PRICING)
|| ImmutableSet.of(CommandName.CREATE).equals(getInstance().allowedEppActions),
"Bulk tokens may only be valid for CREATE actions");
checkArgument(
!getInstance().tokenType.equals(TokenType.BULK_PRICING)
|| !getInstance().discountPremiums,
"Bulk tokens cannot discount premium names");
checkArgument(
getInstance().domainName == null || getInstance().tokenType.isOneTimeUse(),
"Domain name can only be specified for SINGLE_USE or REGISTER_BSA tokens");
checkArgument(
getInstance().redemptionHistoryId == null || getInstance().tokenType.isOneTimeUse(),
"Redemption history entry can only be specified for SINGLE_USE or REGISTER_BSA tokens");
checkArgument(
getInstance().tokenType != TokenType.BULK_PRICING
|| (getInstance().allowedClientIds != null
&& getInstance().allowedClientIds.size() == 1),
"BULK_PRICING tokens must have exactly one allowed client registrar");
checkArgument(
getInstance().discountFraction > 0 || !getInstance().discountPremiums,
"Discount premiums can only be specified along with a discount fraction");
@@ -404,6 +411,44 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda
checkArgumentNotNull(
getInstance().domainName, "ANCHOR_TENANT tokens must be tied to a domain");
}
if (getInstance().registrationBehavior.equals(RegistrationBehavior.NONPREMIUM_CREATE)) {
checkArgument(
getInstance().discountFraction == 0.0,
"NONPREMIUM_CREATE tokens cannot apply a discount");
checkArgumentNotNull(
getInstance().domainName, "NONPREMIUM_CREATE tokens must be tied to a domain");
checkArgument(
getInstance().allowedEppActions == null
|| getInstance().allowedEppActions.contains(CommandName.CREATE),
"NONPREMIUM_CREATE tokens must allow for CREATE actions");
}
checkArgument(
getInstance().renewalPriceBehavior.equals(RenewalPriceBehavior.SPECIFIED)
== (getInstance().renewalPrice != null),
"renewalPrice must be specified iff renewalPriceBehavior is SPECIFIED");
if (getInstance().tokenType.equals(TokenType.BULK_PRICING)) {
checkArgument(
getInstance().discountFraction == 1.0,
"BULK_PRICING tokens must have a discountFraction of 1.0");
checkArgument(
!getInstance().shouldDiscountPremiums(),
"BULK_PRICING tokens cannot discount premium names");
checkArgument(
getInstance().renewalPriceBehavior.equals(RenewalPriceBehavior.SPECIFIED),
"BULK_PRICING tokens must have renewalPriceBehavior set to SPECIFIED");
checkArgument(
getInstance().renewalPrice.getAmount().intValue() == 0,
"BULK_PRICING tokens must have a renewal price of 0");
checkArgument(
ImmutableSet.of(CommandName.CREATE).equals(getInstance().allowedEppActions),
"BULK_PRICING tokens may only be valid for CREATE actions");
checkArgument(
getInstance().allowedClientIds != null && getInstance().allowedClientIds.size() == 1,
"BULK_PRICING tokens must have exactly one allowed client registrar");
}
if (getInstance().domainName != null) {
try {
DomainFlowUtils.validateDomainName(getInstance().domainName);
@@ -500,6 +545,11 @@ public class AllocationToken extends UpdateAutoTimestampEntity implements Builda
return this;
}
public Builder setRenewalPrice(Money renewalPrice) {
getInstance().renewalPrice = renewalPrice;
return this;
}
public Builder setRegistrationBehavior(RegistrationBehavior registrationBehavior) {
getInstance().registrationBehavior = registrationBehavior;
return this;
@@ -46,7 +46,7 @@ public interface PremiumPricingEngine {
private Money createCost;
private Money renewCost;
static DomainPrices create(boolean isPremium, Money createCost, Money renewCost) {
public static DomainPrices create(boolean isPremium, Money createCost, Money renewCost) {
DomainPrices instance = new DomainPrices();
instance.isPremium = isPremium;
instance.createCost = createCost;
@@ -41,9 +41,9 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import com.google.common.collect.Range;
import com.google.common.collect.Sets;
import com.google.common.net.InternetDomainName;
import google.registry.model.Buildable;
import google.registry.model.CacheUtils;
@@ -192,21 +192,19 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
/** Returns the TLD for a given TLD, throwing if none exists. */
public static Tld get(String tld) {
Tld maybeTld = CACHE.get(tld);
if (maybeTld == null) {
throw new TldNotFoundException(tld);
} else {
return maybeTld;
}
return CACHE.get(tld).orElseThrow(() -> new TldNotFoundException(tld));
}
/** Returns the TLD entities for the given TLD strings, throwing if any don't exist. */
public static ImmutableSet<Tld> get(Set<String> tlds) {
Map<String, Tld> registries = CACHE.getAll(tlds);
Map<String, Optional<Tld>> registries = CACHE.getAll(tlds);
ImmutableSet<String> missingRegistries =
Sets.difference(tlds, registries.keySet()).immutableCopy();
registries.entrySet().stream()
.filter(e -> e.getValue().isEmpty())
.map(Map.Entry::getKey)
.collect(toImmutableSet());
if (missingRegistries.isEmpty()) {
return registries.values().stream().collect(toImmutableSet());
return registries.values().stream().map(Optional::get).collect(toImmutableSet());
} else {
throw new TldNotFoundException(missingRegistries);
}
@@ -224,24 +222,24 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
}
/** A cache that loads the {@link Tld} for a given tld. */
private static final LoadingCache<String, Tld> CACHE =
private static final LoadingCache<String, Optional<Tld>> CACHE =
CacheUtils.newCacheBuilder(getSingletonCacheRefreshDuration())
.build(
new CacheLoader<>() {
@Override
public Tld load(final String tld) {
return tm().reTransact(() -> tm().loadByKeyIfPresent(createVKey(tld)))
.orElse(null);
public Optional<Tld> load(final String tld) {
return tm().reTransact(() -> tm().loadByKeyIfPresent(createVKey(tld)));
}
@Override
public Map<? extends String, ? extends Tld> loadAll(Set<? extends String> tlds) {
public Map<? extends String, ? extends Optional<Tld>> loadAll(
Set<? extends String> tlds) {
ImmutableMap<String, VKey<Tld>> keysMap =
tlds.stream().collect(toImmutableMap(tld -> tld, Tld::createVKey));
Map<VKey<? extends Tld>, Tld> entities =
tm().reTransact(() -> tm().loadByKeysIfPresent(keysMap.values()));
return entities.values().stream()
.collect(toImmutableMap(tld -> tld.tldStr, tld -> tld));
return Maps.toMap(
tlds, tld -> Optional.ofNullable(entities.get(createVKey(tld))));
}
});
@@ -114,6 +114,7 @@ import google.registry.ui.server.console.ConsoleDomainListAction;
import google.registry.ui.server.console.ConsoleDumDownloadAction;
import google.registry.ui.server.console.ConsoleEppPasswordAction;
import google.registry.ui.server.console.ConsoleRegistryLockAction;
import google.registry.ui.server.console.ConsoleRegistryLockVerifyAction;
import google.registry.ui.server.console.ConsoleUpdateRegistrarAction;
import google.registry.ui.server.console.ConsoleUserDataAction;
import google.registry.ui.server.console.RegistrarsAction;
@@ -191,6 +192,8 @@ interface RequestComponent {
ConsoleRegistryLockAction consoleRegistryLockAction();
ConsoleRegistryLockVerifyAction consoleRegistryLockVerifyAction();
ConsoleUiAction consoleUiAction();
ConsoleUpdateRegistrarAction consoleUpdateRegistrarAction();
@@ -30,6 +30,7 @@ import google.registry.ui.server.console.ConsoleDomainListAction;
import google.registry.ui.server.console.ConsoleDumDownloadAction;
import google.registry.ui.server.console.ConsoleEppPasswordAction;
import google.registry.ui.server.console.ConsoleRegistryLockAction;
import google.registry.ui.server.console.ConsoleRegistryLockVerifyAction;
import google.registry.ui.server.console.ConsoleUpdateRegistrarAction;
import google.registry.ui.server.console.ConsoleUserDataAction;
import google.registry.ui.server.console.RegistrarsAction;
@@ -69,6 +70,8 @@ public interface FrontendRequestComponent {
ConsoleRegistryLockAction consoleRegistryLockAction();
ConsoleRegistryLockVerifyAction consoleRegistryLockVerifyAction();
ConsoleUiAction consoleUiAction();
ConsoleUpdateRegistrarAction consoleUpdateRegistrarAction();
@@ -76,7 +76,7 @@ public class RdapEntityAction extends RdapActionBase {
// they are global, and might have different roles for different domains.
if (contact.isPresent() && isAuthorized(contact.get())) {
return rdapJsonFormatter.createRdapContactEntity(
contact.get(), ImmutableSet.of(), OutputDataType.FULL);
contact, ImmutableSet.of(), OutputDataType.FULL);
}
}
@@ -461,7 +461,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
.entitySearchResultsBuilder()
.add(
rdapJsonFormatter.createRdapContactEntity(
contact, ImmutableSet.of(), outputDataType));
Optional.of(contact), ImmutableSet.of(), outputDataType));
newCursor =
Optional.of(
CONTACT_CURSOR_PREFIX
@@ -61,6 +61,7 @@ import google.registry.rdap.RdapDataStructures.RdapStatus;
import google.registry.rdap.RdapObjectClasses.RdapContactEntity;
import google.registry.rdap.RdapObjectClasses.RdapDomain;
import google.registry.rdap.RdapObjectClasses.RdapEntity;
import google.registry.rdap.RdapObjectClasses.RdapEntity.Role;
import google.registry.rdap.RdapObjectClasses.RdapNameserver;
import google.registry.rdap.RdapObjectClasses.RdapRegistrarEntity;
import google.registry.rdap.RdapObjectClasses.SecureDns;
@@ -74,6 +75,7 @@ import java.net.InetAddress;
import java.net.URI;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
@@ -249,6 +251,16 @@ public class RdapJsonFormatter {
private static final Ordering<DesignatedContact> DESIGNATED_CONTACT_ORDERING =
Ordering.natural().onResultOf(DesignatedContact::getType);
/**
* The list of RDAP contact roles that are required to be present on each domain.
*
* <p>Per RDAP Response Profile 2.7.3, A domain MUST have the REGISTRANT, ADMIN, TECH roles and
* MAY have others. We also have the BILLING role in our system but it isn't required and is only
* listed if actually present.
*/
private static final ImmutableSet<Role> REQUIRED_CONTACT_ROLES =
ImmutableSet.of(Role.REGISTRANT, Role.ADMIN, Role.TECH);
/** Creates the TOS notice that is added to every reply. */
Notice createTosNotice() {
String linkValue = makeRdapServletRelativeUrl("help", RdapHelpAction.TOS_PATH);
@@ -369,38 +381,53 @@ public class RdapJsonFormatter {
() ->
ImmutableSet.copyOf(replicaTm().loadByKeys(domain.getNameservers()).values()));
// Load the registrant and other contacts and add them to the data.
ImmutableSet<VKey<Contact>> contacts = domain.getReferencedContacts();
ImmutableMap<VKey<? extends Contact>, Contact> loadedContacts =
replicaTm().transact(() -> replicaTm().loadByKeysIfPresent(domain.getReferencedContacts()));
// RDAP Response Profile 2.7.3, A domain MUST have the REGISTRANT, ADMIN, TECH roles and MAY
// have others. We also add the BILLING.
//
contacts.isEmpty()
? ImmutableMap.of()
: replicaTm().transact(() -> replicaTm().loadByKeysIfPresent(contacts));
// RDAP Response Profile 2.7.1, 2.7.3 - we MUST have the contacts. 2.7.4 discusses redaction of
// fields we don't want to show (as opposed to not having contacts at all) because of GDPR etc.
//
// the GDPR redaction is handled in createRdapContactEntity
// The GDPR redaction is handled in createRdapContactEntity.
// Load all contacts that are present and group them by type (it is common for a single contact
// entity to be used across multiple contact types on domain, e.g. registrant and admin).
ImmutableSetMultimap<VKey<Contact>, Type> contactsToRoles =
Streams.concat(
domain.getContacts().stream(),
Stream.of(DesignatedContact.create(Type.REGISTRANT, domain.getRegistrant())))
domain.getAllContacts().stream()
.sorted(DESIGNATED_CONTACT_ORDERING)
.collect(
toImmutableSetMultimap(
DesignatedContact::getContactKey, DesignatedContact::getType));
// Convert the contact entities to RDAP output contacts (this also converts the contact types
// to RDAP roles).
Set<RdapContactEntity> rdapContacts = new LinkedHashSet<>();
for (VKey<Contact> contactKey : contactsToRoles.keySet()) {
Set<RdapEntity.Role> roles =
Set<Role> roles =
contactsToRoles.get(contactKey).stream()
.map(RdapJsonFormatter::convertContactTypeToRdapRole)
.collect(toImmutableSet());
if (roles.isEmpty()) {
continue;
}
builder
.entitiesBuilder()
.add(
createRdapContactEntity(
loadedContacts.get(contactKey), roles, OutputDataType.INTERNAL));
rdapContacts.add(
createRdapContactEntity(
Optional.ofNullable(loadedContacts.get(contactKey)), roles, OutputDataType.INTERNAL));
}
// Loop through all required contact roles and fill in placeholder REDACTED info for any
// required ones that are missing, i.e. because of minimum registration data set.
for (Role role : REQUIRED_CONTACT_ROLES) {
if (rdapContacts.stream().noneMatch(c -> c.roles().contains(role))) {
rdapContacts.add(
createRdapContactEntity(
Optional.empty(), ImmutableSet.of(role), OutputDataType.INTERNAL));
}
}
builder.entitiesBuilder().addAll(rdapContacts);
// Add the nameservers to the data; the load was kicked off above for efficiency.
// RDAP Response Profile 2.9: we MUST have the nameservers
for (Host host : HOST_RESOURCE_ORDERING.immutableSortedCopy(loadedHosts)) {
@@ -502,25 +529,36 @@ public class RdapJsonFormatter {
/**
* Creates a JSON object for a {@link Contact} and associated contact type.
*
* <p>If the contact isn't present (i.e. because of minimum registration data set), then always
* show all of its fields as if they were redacted, and always deny RDAP authorization.
*
* @param contact the contact resource object from which the JSON object should be created
* @param roles the roles of this contact
* @param outputDataType whether to generate full or summary data
*/
RdapContactEntity createRdapContactEntity(
Contact contact, Iterable<RdapEntity.Role> roles, OutputDataType outputDataType) {
Optional<Contact> contact, Iterable<RdapEntity.Role> roles, OutputDataType outputDataType) {
RdapContactEntity.Builder contactBuilder = RdapContactEntity.builder();
// RDAP Response Profile 2.7.1, 2.7.3 - we MUST have the contacts. 2.7.4 discusses censoring of
// fields we don't want to show (as opposed to not having contacts at all) because of GDPR etc.
//
// 2.8 allows for unredacted output for authorized people.
// TODO(mcilwain): Once the RDAP profile is fully updated for minimum registration data set,
// we will want to not include non-existent contacts at all, rather than
// pretending they exist and just showing REDACTED info. This is especially
// important for authorized flows, where you wouldn't expect to see redaction
// (although no one actually has access to authorized flows yet).
boolean isAuthorized =
rdapAuthorization.isAuthorizedForRegistrar(contact.getCurrentSponsorRegistrarId());
contact.isPresent()
&& rdapAuthorization.isAuthorizedForRegistrar(
contact.get().getCurrentSponsorRegistrarId());
VcardArray.Builder vcardBuilder = VcardArray.builder();
if (isAuthorized) {
fillRdapContactEntityWhenAuthorized(contactBuilder, vcardBuilder, contact, outputDataType);
fillRdapContactEntityWhenAuthorized(
contactBuilder, vcardBuilder, contact.get(), outputDataType);
} else {
// GTLD Registration Data Temp Spec 17may18, Appendix A, 2.3, 2.4 and RDAP Response Profile
// 2.7.4.1, 2.7.4.2 - the following fields must be redacted:
@@ -20,7 +20,6 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
import com.google.common.base.Ascii;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import google.registry.model.contact.Contact;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.Domain;
@@ -45,12 +44,11 @@ import google.registry.xjc.rgp.XjcRgpStatusType;
import google.registry.xjc.rgp.XjcRgpStatusValueType;
import google.registry.xjc.secdns.XjcSecdnsDsDataType;
import google.registry.xjc.secdns.XjcSecdnsDsOrKeyType;
import java.util.Optional;
/** Utility class that turns {@link Domain} as {@link XjcRdeDomainElement}. */
final class DomainToXjcConverter {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/** Converts {@link Domain} to {@link XjcRdeDomainElement}. */
static XjcRdeDomainElement convert(Domain domain, RdeMode mode) {
return new XjcRdeDomainElement(convertDomain(domain, mode));
@@ -168,11 +166,9 @@ final class DomainToXjcConverter {
// o An OPTIONAL <registrant> element that contain the identifier for
// the human or organizational social information object associated
// as the holder of the domain name object.
VKey<Contact> registrant = model.getRegistrant();
if (registrant == null) {
logger.atWarning().log("Domain %s has no registrant contact.", domainName);
} else {
Contact registrantContact = tm().transact(() -> tm().loadByKey(registrant));
Optional<VKey<Contact>> registrant = model.getRegistrant();
if (registrant.isPresent()) {
Contact registrantContact = tm().transact(() -> tm().loadByKey(registrant.get()));
checkState(
registrantContact != null,
"Registrant contact %s on domain %s does not exist",
@@ -0,0 +1,55 @@
// 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.tools;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.model.common.FeatureFlag;
import google.registry.model.common.FeatureFlag.FeatureName;
import google.registry.model.common.FeatureFlag.FeatureStatus;
import google.registry.tools.params.TransitionListParameter.FeatureStatusTransitions;
import java.util.List;
import java.util.Optional;
import org.joda.time.DateTime;
/** Command for creating and updating {@link FeatureFlag} objects. */
@Parameters(separators = " =", commandDescription = "Create or update a feature flag.")
public class ConfigureFeatureFlagCommand extends MutatingCommand {
@Parameter(description = "Feature flag name(s) to create or update", required = true)
private List<FeatureName> mainParameters;
@Parameter(
names = "--status_map",
converter = FeatureStatusTransitions.class,
validateWith = FeatureStatusTransitions.class,
description =
"Comma-delimited list of feature status transitions effective on specific dates, of the"
+ " form <time>=<status>[,<time>=<status>]* where each status represents the status"
+ " of the feature flag.",
required = true)
private ImmutableSortedMap<DateTime, FeatureStatus> featureStatusTransitions;
@Override
protected void init() throws Exception {
for (FeatureName name : mainParameters) {
Optional<FeatureFlag> oldFlag = FeatureFlag.getUncached(name);
FeatureFlag.Builder newFlagBuilder =
new FeatureFlag().asBuilder().setFeatureName(name).setStatusMap(featureStatusTransitions);
stageEntityChange(oldFlag.orElse(null), newFlagBuilder.build());
}
}
}
@@ -111,8 +111,8 @@ abstract class CreateOrUpdateBulkPricingPackageCommand extends MutatingCommand {
if (clearLastNotificationSent()) {
builder.setLastNotificationSent(null);
}
BulkPricingPackage newBUlkPricingPackage = builder.build();
stageEntityChange(oldBulkPricingPackage, newBUlkPricingPackage);
BulkPricingPackage newBulkPricingPackage = builder.build();
stageEntityChange(oldBulkPricingPackage, newBulkPricingPackage);
});
}
}
@@ -15,30 +15,25 @@
package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.model.console.User.grantIapPermission;
import com.beust.jcommander.Parameters;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import google.registry.batch.CloudTasksUtils;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import google.registry.tools.server.UpdateUserGroupAction;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.inject.Inject;
/** Command to create a new User. */
@Parameters(separators = " =", commandDescription = "Update a user account")
public class CreateUserCommand extends CreateOrUpdateUserCommand implements CommandWithConnection {
static final String IAP_SECURED_WEB_APP_USER_ROLE = "roles/iap.httpsResourceAccessor";
static final FluentLogger logger = FluentLogger.forEnclosingClass();
private ServiceConnection connection;
public class CreateUserCommand extends CreateOrUpdateUserCommand {
@Inject IamClient iamClient;
@Inject CloudTasksUtils cloudTasksUtils;
@Inject
@Config("gSuiteConsoleUserGroupEmailAddress")
Optional<String> maybeGroupEmailAddress;
@@ -53,29 +48,7 @@ public class CreateUserCommand extends CreateOrUpdateUserCommand implements Comm
@Override
protected String execute() throws Exception {
String ret = super.execute();
String groupEmailAddress = maybeGroupEmailAddress.orElse(null);
if (groupEmailAddress != null) {
logger.atInfo().log("Adding %s to group %s", email, groupEmailAddress);
connection.sendPostRequest(
UpdateUserGroupAction.PATH,
ImmutableMap.of(
"userEmailAddress",
email,
"groupEmailAddress",
groupEmailAddress,
"groupUpdateMode",
"ADD"),
MediaType.PLAIN_TEXT_UTF_8,
new byte[0]);
} else {
logger.atInfo().log("Granting IAP role to user %s", email);
iamClient.addBinding(email, IAP_SECURED_WEB_APP_USER_ROLE);
}
grantIapPermission(email, maybeGroupEmailAddress, cloudTasksUtils, iamClient);
return ret;
}
@Override
public void setConnection(ServiceConnection connection) {
this.connection = connection;
}
}
@@ -15,32 +15,27 @@
package google.registry.tools;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.tools.CreateUserCommand.IAP_SECURED_WEB_APP_USER_ROLE;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import google.registry.batch.CloudTasksUtils;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import google.registry.tools.server.UpdateUserGroupAction;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.inject.Inject;
/** Deletes a {@link User}. */
@Parameters(separators = " =", commandDescription = "Delete a user account")
public class DeleteUserCommand extends ConfirmingCommand implements CommandWithConnection {
public class DeleteUserCommand extends ConfirmingCommand {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private ServiceConnection connection;
@Inject IamClient iamClient;
@Inject CloudTasksUtils cloudTasksUtils;
@Inject
@Config("gSuiteConsoleUserGroupEmailAddress")
Optional<String> maybeGroupEmailAddress;
@@ -49,11 +44,6 @@ public class DeleteUserCommand extends ConfirmingCommand implements CommandWithC
@Parameter(names = "--email", description = "Email address of the user", required = true)
String email;
@Override
public void setConnection(ServiceConnection connection) {
this.connection = connection;
}
@Override
protected String prompt() {
checkArgumentNotNull(email, "Email must be provided");
@@ -69,24 +59,7 @@ public class DeleteUserCommand extends ConfirmingCommand implements CommandWithC
checkArgumentPresent(optionalUser, "Email no longer corresponds to a valid user");
tm().delete(optionalUser.get());
});
String groupEmailAddress = maybeGroupEmailAddress.orElse(null);
if (groupEmailAddress != null) {
logger.atInfo().log("Removing %s from group %s", email, groupEmailAddress);
connection.sendPostRequest(
UpdateUserGroupAction.PATH,
ImmutableMap.of(
"userEmailAddress",
email,
"groupEmailAddress",
groupEmailAddress,
"groupUpdateMode",
"REMOVE"),
MediaType.PLAIN_TEXT_UTF_8,
new byte[0]);
} else {
logger.atInfo().log("Removing IAP role from user %s", email);
iamClient.removeBinding(email, IAP_SECURED_WEB_APP_USER_ROLE);
}
User.revokeIapPermission(email, maybeGroupEmailAddress, cloudTasksUtils, iamClient);
return String.format("Deleted user with email %s", email);
}
}
@@ -30,6 +30,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.beust.jcommander.internal.Nullable;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
@@ -62,6 +63,7 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.inject.Inject;
import javax.inject.Named;
import org.joda.money.Money;
import org.joda.time.DateTime;
/** Command to generate and persist {@link AllocationToken}s. */
@@ -158,10 +160,16 @@ class GenerateAllocationTokensCommand implements Command {
"The type of renewal price behavior, either DEFAULT (default), NONPREMIUM, or SPECIFIED."
+ " This indicates how a domain should be charged for renewal. By default, a domain"
+ " will be renewed at the renewal price from the pricing engine. If the renewal"
+ " price behavior is set to SPECIFIED, it means that the renewal cost will be the"
+ " same as the domain's calculated create price.")
+ " price behavior is set to SPECIFIED, it means that the renewal cost will be equal"
+ " to the provided renewal price.")
private RenewalPriceBehavior renewalPriceBehavior = DEFAULT;
@Parameter(
names = {"--renewal_price"},
description = "The renewal price amount iff the renewal price behavior is SPECIFIED.")
@Nullable
private Money renewalPrice;
@Parameter(
names = {"--registration_behavior"},
description =
@@ -228,6 +236,7 @@ class GenerateAllocationTokensCommand implements Command {
Optional.ofNullable(discountYears).ifPresent(token::setDiscountYears);
Optional.ofNullable(tokenStatusTransitions)
.ifPresent(token::setTokenStatusTransitions);
Optional.ofNullable(renewalPrice).ifPresent(token::setRenewalPrice);
Optional.ofNullable(domainNames)
.ifPresent(d -> token.setDomainName(d.removeFirst()));
return token.build();
@@ -295,6 +304,10 @@ class GenerateAllocationTokensCommand implements Command {
+ " map");
}
checkArgument(
renewalPriceBehavior.equals(RenewalPriceBehavior.SPECIFIED) == (renewalPrice != null),
"renewal_price must be specified iff renewal_price_behavior is SPECIFIED");
if (tokenStrings != null) {
verifyTokenStringsDoNotExist();
}
@@ -0,0 +1,53 @@
// 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.tools;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.fasterxml.jackson.databind.ObjectMapper;
import google.registry.model.common.FeatureFlag;
import google.registry.model.common.FeatureFlag.FeatureFlagNotFoundException;
import google.registry.model.common.FeatureFlag.FeatureName;
import java.io.PrintStream;
import java.util.List;
import java.util.Optional;
import javax.inject.Inject;
/** Command to show a {@link FeatureFlag}. */
@Parameters(separators = " =", commandDescription = "Show FeatureFlag record(s)")
public class GetFeatureFlagCommand implements Command {
@Parameter(description = "Feature flag(s) to show", required = true)
private List<FeatureName> mainParameters;
@Inject ObjectMapper objectMapper;
@Override
public void run() throws Exception {
// Don't use try-with-resources to manage standard output streams, closing the stream will
// cause subsequent output to standard output or standard error to be lost
// See: https://errorprone.info/bugpattern/ClosingStandardOutputStreams
PrintStream printStream = new PrintStream(System.out, false, UTF_8);
for (FeatureName featureFlag : mainParameters) {
Optional<FeatureFlag> maybeFeatureFlag = FeatureFlag.getUncached(featureFlag);
if (maybeFeatureFlag.isEmpty()) {
throw new FeatureFlagNotFoundException(featureFlag);
}
printStream.println(objectMapper.writeValueAsString(maybeFeatureFlag.get()));
}
}
}
@@ -20,7 +20,7 @@ import com.google.api.services.cloudresourcemanager.model.GetIamPolicyRequest;
import com.google.api.services.cloudresourcemanager.model.Policy;
import com.google.api.services.cloudresourcemanager.model.SetIamPolicyRequest;
import com.google.common.base.Ascii;
import google.registry.config.CredentialModule.LocalCredential;
import google.registry.config.CredentialModule.ApplicationDefaultCredential;
import google.registry.config.RegistryConfig.Config;
import google.registry.util.GoogleCredentialsBundle;
import java.io.IOException;
@@ -38,7 +38,7 @@ public class IamClient {
@Inject
public IamClient(
@LocalCredential GoogleCredentialsBundle credentialsBundle,
@ApplicationDefaultCredential GoogleCredentialsBundle credentialsBundle,
@Config("projectId") String projectId) {
this(
new CloudResourceManager.Builder(
@@ -0,0 +1,43 @@
// 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.tools;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.beust.jcommander.Parameters;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableList;
import google.registry.model.common.FeatureFlag;
import java.io.PrintStream;
import javax.inject.Inject;
/** Command to list all {@link google.registry.model.common.FeatureFlag} objects. */
@Parameters(separators = " =", commandDescription = "List all feature flags.")
public class ListFeatureFlagsCommand implements Command {
@Inject ObjectMapper mapper;
@Override
public void run() throws Exception {
// Don't use try-with-resources to manage standard output streams, closing the stream will
// cause subsequent output to standard output or standard error to be lost
// See: https://errorprone.info/bugpattern/ClosingStandardOutputStreams
PrintStream printStream = new PrintStream(System.out, false, UTF_8);
ImmutableList<FeatureFlag> featureFlags = FeatureFlag.getAllUncached();
for (FeatureFlag featureFlag : featureFlags) {
printStream.println(mapper.writeValueAsString(featureFlag));
}
}
}
@@ -33,6 +33,7 @@ public final class RegistryTool {
.put("canonicalize_labels", CanonicalizeLabelsCommand.class)
.put("check_domain", CheckDomainCommand.class)
.put("check_domain_claims", CheckDomainClaimsCommand.class)
.put("configure_feature_flag", ConfigureFeatureFlagCommand.class)
.put("configure_tld", ConfigureTldCommand.class)
.put("convert_idn", ConvertIdnCommand.class)
.put("count_domains", CountDomainsCommand.class)
@@ -71,6 +72,7 @@ public final class RegistryTool {
.put("get_claims_list", GetClaimsListCommand.class)
.put("get_contact", GetContactCommand.class)
.put("get_domain", GetDomainCommand.class)
.put("get_feature_flag", GetFeatureFlagCommand.class)
.put("get_history_entries", GetHistoryEntriesCommand.class)
.put("get_host", GetHostCommand.class)
.put("get_keyring_secret", GetKeyringSecretCommand.class)
@@ -85,6 +87,7 @@ public final class RegistryTool {
.put("hash_certificate", HashCertificateCommand.class)
.put("list_cursors", ListCursorsCommand.class)
.put("list_domains", ListDomainsCommand.class)
.put("list_feature_flags", ListFeatureFlagsCommand.class)
.put("list_hosts", ListHostsCommand.class)
.put("list_premium_lists", ListPremiumListsCommand.class)
.put("list_registrars", ListRegistrarsCommand.class)
@@ -119,6 +119,8 @@ interface RegistryToolComponent {
void inject(GetDomainCommand command);
void inject(GetFeatureFlagCommand command);
void inject(GetHostCommand command);
void inject(GetKeyringSecretCommand command);
@@ -131,6 +133,8 @@ interface RegistryToolComponent {
void inject(ListCursorsCommand command);
void inject(ListFeatureFlagsCommand command);
void inject(LockDomainCommand command);
void inject(LoginCommand command);
@@ -17,7 +17,7 @@ package google.registry.tools;
import com.google.api.services.dataflow.Dataflow;
import dagger.Module;
import dagger.Provides;
import google.registry.config.CredentialModule.LocalCredential;
import google.registry.config.CredentialModule.ApplicationDefaultCredential;
import google.registry.config.RegistryConfig.Config;
import google.registry.util.GoogleCredentialsBundle;
@@ -27,7 +27,7 @@ public class RegistryToolDataflowModule {
@Provides
static Dataflow provideDataflow(
@LocalCredential GoogleCredentialsBundle credentialsBundle,
@ApplicationDefaultCredential GoogleCredentialsBundle credentialsBundle,
@Config("projectId") String projectId) {
return new Dataflow.Builder(
credentialsBundle.getHttpTransport(),
@@ -22,6 +22,8 @@ import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.MoreFiles;
import google.registry.batch.CloudTasksUtils;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.OteAccountBuilder;
import google.registry.tools.params.PathParameter;
import google.registry.util.Clock;
@@ -30,6 +32,7 @@ import google.registry.util.StringGenerator;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import javax.inject.Inject;
import javax.inject.Named;
@@ -47,7 +50,7 @@ final class SetupOteCommand extends ConfirmingCommand {
@Parameter(
names = {"-a", "--ip_allow_list"},
description = "Comma-separated list of IP addreses or CIDR ranges.",
description = "Comma-separated list of IP addresses or CIDR ranges.",
required = true)
private List<String> ipAllowList = new ArrayList<>();
@@ -55,7 +58,7 @@ final class SetupOteCommand extends ConfirmingCommand {
names = {"--email"},
description =
"The registrar's account to use for console access. "
+ "Must be on the registry's G Suite domain.",
+ "Must be on the registry's Google Workspace domain.",
required = true)
private String email;
@@ -76,6 +79,14 @@ final class SetupOteCommand extends ConfirmingCommand {
@Inject Clock clock;
@Inject CloudTasksUtils cloudTasksUtils;
@Inject IamClient iamClient;
@Inject
@Config("gSuiteConsoleUserGroupEmailAddress")
Optional<String> maybeGroupEmailAddress;
OteAccountBuilder oteAccountBuilder;
String password;
@@ -87,7 +98,7 @@ final class SetupOteCommand extends ConfirmingCommand {
password = passwordGenerator.createString(PASSWORD_LENGTH);
oteAccountBuilder =
OteAccountBuilder.forRegistrarId(registrar)
.addContact(email)
.addUser(email)
.setPassword(password)
.setIpAllowList(ipAllowList)
.setReplaceExisting(overwrite);
@@ -114,8 +125,11 @@ final class SetupOteCommand extends ConfirmingCommand {
&& RegistryEnvironment.get() != RegistryEnvironment.UNITTEST) {
builder.append(
String.format(
"\n\nWARNING: Running against %s environment. Are "
+ "you sure you didn\'t mean to run this against sandbox (e.g. \"-e SANDBOX\")?",
"""
WARNING: Running against %s environment. Are \
you sure you didn't mean to run this against sandbox (e.g. "-e SANDBOX")?""",
RegistryEnvironment.get()));
}
@@ -125,15 +139,15 @@ final class SetupOteCommand extends ConfirmingCommand {
@Override
public String execute() {
ImmutableMap<String, String> clientIdToTld = oteAccountBuilder.buildAndPersist();
oteAccountBuilder.grantIapPermission(maybeGroupEmailAddress, cloudTasksUtils, iamClient);
StringBuilder output = new StringBuilder();
output.append("Copy these usernames/passwords back into the onboarding bug:\n\n");
clientIdToTld.forEach(
(clientId, tld) -> {
output.append(
String.format("Login: %s\nPassword: %s\nTLD: %s\n\n", clientId, password, tld));
});
(clientId, tld) ->
output.append(
String.format("Login: %s\nPassword: %s\nTLD: %s\n\n", clientId, password, tld)));
return output.toString();
}
@@ -40,6 +40,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import org.joda.money.Money;
import org.joda.time.DateTime;
/** Command to update existing {@link AllocationToken}s. */
@@ -110,11 +111,17 @@ final class UpdateAllocationTokensCommand extends UpdateOrDeleteAllocationTokens
"The type of renewal price behavior, either DEFAULT (default), NONPREMIUM, or SPECIFIED."
+ " This indicates how a domain should be charged for renewal. By default, a domain"
+ " will be renewed at the renewal price from the pricing engine. If the renewal"
+ " price behavior is set to SPECIFIED, it means that the renewal cost will be the"
+ " same as the domain's calculated create price.")
+ " price behavior is set to SPECIFIED, it means that the renewal cost will be equal"
+ " to the provided renewal price.")
@Nullable
private RenewalPriceBehavior renewalPriceBehavior;
@Parameter(
names = {"--renewal_price"},
description = "The renewal price amount iff the renewal price behavior is SPECIFIED.")
@Nullable
private Money renewalPrice;
@Parameter(
names = {"--registration_behavior"},
description =
@@ -189,17 +196,22 @@ final class UpdateAllocationTokensCommand extends UpdateOrDeleteAllocationTokens
.ifPresent(tlds -> builder.setAllowedTlds(ImmutableSet.copyOf(tlds)));
Optional.ofNullable(allowedEppActions)
.ifPresent(
eppActions -> {
builder.setAllowedEppActions(
eppActions.stream()
.map(CommandName::parseKnownCommand)
.collect(toImmutableSet()));
});
eppActions ->
builder.setAllowedEppActions(
eppActions.stream()
.map(CommandName::parseKnownCommand)
.collect(toImmutableSet())));
Optional.ofNullable(discountFraction).ifPresent(builder::setDiscountFraction);
Optional.ofNullable(discountPremiums).ifPresent(builder::setDiscountPremiums);
Optional.ofNullable(discountYears).ifPresent(builder::setDiscountYears);
Optional.ofNullable(tokenStatusTransitions).ifPresent(builder::setTokenStatusTransitions);
if (renewalPriceBehavior != null
&& renewalPriceBehavior != original.getRenewalPriceBehavior()) {
builder.setRenewalPrice(null);
}
Optional.ofNullable(renewalPriceBehavior).ifPresent(builder::setRenewalPriceBehavior);
Optional.ofNullable(renewalPrice).ifPresent(builder::setRenewalPrice);
Optional.ofNullable(registrationBehavior).ifPresent(builder::setRegistrationBehavior);
return builder.build();
}
@@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkArgument;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Ordering;
import google.registry.model.common.FeatureFlag.FeatureStatus;
import google.registry.model.domain.token.AllocationToken.TokenStatus;
import google.registry.model.tld.Tld.TldState;
import org.joda.money.Money;
@@ -72,4 +73,12 @@ public abstract class TransitionListParameter<V> extends KeyValueMapParameter<Da
return TokenStatus.valueOf(value);
}
}
/** Converter-validator for feature status transitions. */
public static class FeatureStatusTransitions extends TransitionListParameter<FeatureStatus> {
@Override
protected FeatureStatus parseValue(String value) {
return FeatureStatus.valueOf(value);
}
}
}
@@ -34,6 +34,7 @@ import javax.inject.Inject;
public class UpdateUserGroupAction implements Runnable {
public static final String PATH = "/_dr/admin/updateUserGroup";
public static final String GROUP_UPDATE_QUEUE = "console-user-group-update";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -53,7 +54,7 @@ public class UpdateUserGroupAction implements Runnable {
@Inject
UpdateUserGroupAction() {}
enum Mode {
public enum Mode {
ADD,
REMOVE
}
@@ -254,5 +254,4 @@ public abstract class ConsoleApiAction implements Runnable {
super(message);
}
}
}
@@ -47,10 +47,8 @@ import google.registry.util.EmailMessage;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
import jakarta.servlet.http.HttpServletRequest;
import java.net.URISyntaxException;
import java.util.Optional;
import javax.inject.Inject;
import org.apache.http.client.utils.URIBuilder;
import org.joda.time.Duration;
/**
@@ -153,14 +151,9 @@ public class ConsoleRegistryLockAction extends ConsoleApiAction {
private void sendVerificationEmail(RegistryLock lock, String userEmail, boolean isLock) {
try {
String url =
new URIBuilder()
.setScheme("https")
.setHost(consoleApiParams.request().getServerName())
// TODO: replace this with the PATH in ConsoleRegistryLockVerifyAction once it exists
.setPath("/console-api/registry-lock-verify")
.setParameter("lockVerificationCode", lock.getVerificationCode())
.build()
.toString();
String.format(
"https://%s/console/#/registry-lock-verify?lockVerificationCode=%s",
consoleApiParams.request().getServerName(), lock.getVerificationCode());
String body = String.format(VERIFICATION_EMAIL_TEMPLATE, lock.getDomainName(), url);
ImmutableList<InternetAddress> recipients =
ImmutableList.of(new InternetAddress(userEmail, true));
@@ -171,7 +164,7 @@ public class ConsoleRegistryLockAction extends ConsoleApiAction {
.setSubject(String.format("Registry %s verification", action))
.setRecipients(recipients)
.build());
} catch (AddressException | URISyntaxException e) {
} catch (AddressException e) {
throw new RuntimeException(e); // caught above -- this is so we can run in a transaction
}
}
@@ -0,0 +1,80 @@
// 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.ui.server.console;
import static google.registry.request.Action.Method.GET;
import com.google.common.base.Ascii;
import com.google.gson.Gson;
import com.google.gson.annotations.Expose;
import google.registry.model.console.User;
import google.registry.model.domain.RegistryLock;
import google.registry.request.Action;
import google.registry.request.Parameter;
import google.registry.request.auth.Auth;
import google.registry.tools.DomainLockUtils;
import google.registry.ui.server.registrar.ConsoleApiParams;
import jakarta.servlet.http.HttpServletResponse;
import javax.inject.Inject;
/** Handler for verifying registry lock requests, a form of 2FA. */
@Action(
service = Action.Service.DEFAULT,
path = ConsoleRegistryLockVerifyAction.PATH,
method = {GET},
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public class ConsoleRegistryLockVerifyAction extends ConsoleApiAction {
static final String PATH = "/console-api/registry-lock-verify";
private final DomainLockUtils domainLockUtils;
private final Gson gson;
private final String lockVerificationCode;
@Inject
public ConsoleRegistryLockVerifyAction(
ConsoleApiParams consoleApiParams,
DomainLockUtils domainLockUtils,
Gson gson,
@Parameter("lockVerificationCode") String lockVerificationCode) {
super(consoleApiParams);
this.domainLockUtils = domainLockUtils;
this.gson = gson;
this.lockVerificationCode = lockVerificationCode;
}
@Override
protected void getHandler(User user) {
RegistryLock lock =
domainLockUtils.verifyVerificationCode(lockVerificationCode, user.getUserRoles().isAdmin());
RegistryLockAction action =
lock.getLockCompletionTime().isPresent()
? RegistryLockAction.UNLOCKED
: RegistryLockAction.LOCKED;
RegistryLockVerificationResponse lockResponse =
new RegistryLockVerificationResponse(
Ascii.toLowerCase(action.toString()), lock.getDomainName(), lock.getRegistrarId());
consoleApiParams.response().setPayload(gson.toJson(lockResponse));
consoleApiParams.response().setStatus(HttpServletResponse.SC_OK);
}
private enum RegistryLockAction {
LOCKED,
UNLOCKED
}
private record RegistryLockVerificationResponse(
@Expose String action, @Expose String domainName, @Expose String registrarId) {}
}
@@ -47,8 +47,7 @@ public class ConsoleUpdateRegistrarAction extends ConsoleApiAction {
@Inject
ConsoleUpdateRegistrarAction(
ConsoleApiParams consoleApiParams,
@Parameter("registrar") Optional<Registrar> registrar) {
ConsoleApiParams consoleApiParams, @Parameter("registrar") Optional<Registrar> registrar) {
super(consoleApiParams);
this.registrar = registrar;
}
@@ -108,5 +107,4 @@ public class ConsoleUpdateRegistrarAction extends ConsoleApiAction {
consoleApiParams.response().setStatus(SC_OK);
}
}
@@ -24,6 +24,8 @@ import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import com.google.template.soy.tofu.SoyTofu;
import google.registry.batch.CloudTasksUtils;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.OteAccountBuilder;
import google.registry.request.Action;
import google.registry.request.Action.Method;
@@ -31,6 +33,7 @@ import google.registry.request.HttpException.BadRequestException;
import google.registry.request.Parameter;
import google.registry.request.auth.Auth;
import google.registry.request.auth.AuthenticatedRegistrarAccessor;
import google.registry.tools.IamClient;
import google.registry.ui.server.SendEmailUtils;
import google.registry.ui.server.SoyTemplateUtils;
import google.registry.ui.soy.registrar.OteSetupConsoleSoyInfo;
@@ -87,6 +90,14 @@ public final class ConsoleOteSetupAction extends HtmlAction {
@Parameter("password")
Optional<String> optionalPassword;
@Inject CloudTasksUtils cloudTasksUtils;
@Inject IamClient iamClient;
@Inject
@Config("gSuiteConsoleUserGroupEmailAddress")
Optional<String> maybeGroupEmailAddress;
@Inject
ConsoleOteSetupAction() {}
@@ -107,16 +118,11 @@ public final class ConsoleOteSetupAction extends HtmlAction {
return;
}
switch (method) {
case POST -> {
runPost(data);
}
case GET -> {
runGet(data);
}
default -> {
throw new BadRequestException(
String.format("Action cannot be called with method %s", method));
}
case POST -> runPost(data);
case GET -> runGet(data);
default ->
throw new BadRequestException(
String.format("Action cannot be called with method %s", method));
}
}
@@ -133,11 +139,13 @@ public final class ConsoleOteSetupAction extends HtmlAction {
data.put("contactEmail", email.get());
String password = optionalPassword.orElse(passwordGenerator.createString(PASSWORD_LENGTH));
ImmutableMap<String, String> clientIdToTld =
OteAccountBuilder oteAccountBuilder =
OteAccountBuilder.forRegistrarId(clientId.get())
.addContact(email.get())
.setPassword(password)
.buildAndPersist();
.addUser(email.get())
.setPassword(password);
ImmutableMap<String, String> clientIdToTld = oteAccountBuilder.buildAndPersist();
oteAccountBuilder.grantIapPermission(maybeGroupEmailAddress, cloudTasksUtils, iamClient);
sendExternalUpdates(clientIdToTld);
@@ -26,10 +26,15 @@ import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import com.google.template.soy.tofu.SoyTofu;
import google.registry.batch.CloudTasksUtils;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.console.RegistrarRole;
import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import google.registry.model.console.UserRoles;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarAddress;
import google.registry.model.registrar.RegistrarBase.State;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.request.Action;
import google.registry.request.Action.Method;
import google.registry.request.Action.Service;
@@ -37,6 +42,7 @@ import google.registry.request.HttpException.BadRequestException;
import google.registry.request.Parameter;
import google.registry.request.auth.Auth;
import google.registry.request.auth.AuthenticatedRegistrarAccessor;
import google.registry.tools.IamClient;
import google.registry.ui.server.SendEmailUtils;
import google.registry.ui.server.SoyTemplateUtils;
import google.registry.ui.soy.registrar.AnalyticsSoyInfo;
@@ -95,6 +101,14 @@ public final class ConsoleRegistrarCreatorAction extends HtmlAction {
@Parameter("consoleName")
Optional<String> name;
@Inject CloudTasksUtils cloudTasksUtils;
@Inject IamClient iamClient;
@Inject
@Config("gSuiteConsoleUserGroupEmailAddress")
Optional<String> maybeGroupEmailAddress;
@Inject @Parameter("billingAccount") Optional<String> billingAccount;
@Inject @Parameter("ianaId") Optional<Integer> ianaId;
@Inject @Parameter("referralEmail") Optional<String> referralEmail;
@@ -129,16 +143,11 @@ public final class ConsoleRegistrarCreatorAction extends HtmlAction {
return;
}
switch (method) {
case POST -> {
runPost(data);
}
case GET -> {
runGet(data);
}
default -> {
throw new BadRequestException(
String.format("Action cannot be called with method %s", method));
}
case POST -> runPost(data);
case GET -> runGet(data);
default ->
throw new BadRequestException(
String.format("Action cannot be called with method %s", method));
}
}
@@ -169,7 +178,8 @@ public final class ConsoleRegistrarCreatorAction extends HtmlAction {
list))
.collect(
toImmutableMap(
list -> CurrencyUnit.of(Ascii.toUpperCase(list.get(0))), list -> list.get(1)));
list -> CurrencyUnit.of(Ascii.toUpperCase(list.getFirst())),
list -> list.get(1)));
} catch (Throwable e) {
throw new RuntimeException("Error parsing billing accounts - " + e.getMessage(), e);
}
@@ -233,12 +243,15 @@ public final class ConsoleRegistrarCreatorAction extends HtmlAction {
.setZip(optionalZip.orElse(null))
.build())
.build();
RegistrarPoc contact =
new RegistrarPoc.Builder()
.setRegistrar(registrar)
.setName(consoleUserEmail.get())
User user =
new User.Builder()
.setEmailAddress(consoleUserEmail.get())
.setLoginEmailAddress(consoleUserEmail.get())
.setUserRoles(
new UserRoles.Builder()
.setRegistrarRoles(
ImmutableMap.of(
registrar.getRegistrarId(), RegistrarRole.ACCOUNT_MANAGER))
.build())
.build();
tm().transact(
() -> {
@@ -246,8 +259,11 @@ public final class ConsoleRegistrarCreatorAction extends HtmlAction {
Registrar.loadByRegistrarId(registrar.getRegistrarId()).isEmpty(),
"Registrar with client ID %s already exists",
registrar.getRegistrarId());
tm().putAll(registrar, contact);
tm().put(registrar);
});
UserDao.saveUser(user);
User.grantIapPermission(
user.getEmailAddress(), maybeGroupEmailAddress, cloudTasksUtils, iamClient);
data.put("password", password);
data.put("passcode", phonePasscode);
@@ -88,22 +88,6 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA
static final String ARGS_PARAM = "args";
static final String ID_PARAM = "id";
/**
* Allows task enqueueing to be disabled when executing registrar console test cases.
*
* <p>The existing workflow in UI test cases triggers task enqueueing, which was not an issue with
* Task Queue since it's a native App Engine feature simulated by the App Engine SDK's
* environment. However, with Cloud Tasks, the server enqueues and fails to deliver to the actual
* Cloud Tasks endpoint due to lack of permission.
*
* <p>One way to allow enqueuing in backend test and avoid enqueuing in UI test is to disable
* enqueuing when the test server starts and enable enqueueing once the test server stops. This
* can be done by utilizing a ThreadLocal<Boolean> variable isInTestDriver, which is set to false
* by default. Enqueuing is allowed only if the value of isInTestDriver is false. It's set to true
* in start() and set to false in stop() inside TestDriver.java, a class used in testing.
*/
private static final ThreadLocal<Boolean> isInTestDriver = ThreadLocal.withInitial(() -> false);
@Inject JsonActionRunner jsonActionRunner;
@Inject RegistrarConsoleMetrics registrarConsoleMetrics;
@Inject SendEmailUtils sendEmailUtils;
@@ -118,14 +102,6 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA
return contact.getPhoneNumber() != null;
}
public static void setIsInTestDriverToFalse() {
isInTestDriver.set(false);
}
public static void setIsInTestDriverToTrue() {
isInTestDriver.set(true);
}
@Override
public void run() {
jsonActionRunner.run(this);
@@ -623,7 +599,7 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA
if (CollectionUtils.difference(changedKeys, "lastUpdateTime").isEmpty()) {
return;
}
if (!isInTestDriver.get()) {
if (!RegistryEnvironment.isInTestServer()) {
// Enqueues a sync registrar sheet task if enqueuing is not triggered by console tests and
// there's an update besides the lastUpdateTime
cloudTasksUtils.enqueue(
@@ -105,7 +105,9 @@ final class DomainWhoisResponse extends WhoisResponseImpl {
"Registrar Abuse Contact Phone",
abuseContact.map(RegistrarPoc::getPhoneNumber).orElse(""))
.emitStatusValues(domain.getStatusValues(), domain.getGracePeriods())
.emitContact("Registrant", Optional.of(domain.getRegistrant()), preferUnicode)
// TODO(mcilwain): Investigate if the WHOIS spec requires us to always output REDACTED
// text in WHOIS even if the contact is not present, and if so, do so.
.emitContact("Registrant", domain.getRegistrant(), preferUnicode)
.emitContact("Admin", getContactReference(Type.ADMIN), preferUnicode)
.emitContact("Tech", getContactReference(Type.TECH), preferUnicode)
.emitContact("Billing", getContactReference(Type.BILLING), preferUnicode)
@@ -20,6 +20,7 @@ import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistEppResource;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.LogsSubject.assertAboutLogs;
import static org.joda.money.CurrencyUnit.USD;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
@@ -111,8 +112,9 @@ public class CheckBulkComplianceActionTest {
.setAllowedTlds(ImmutableSet.of("foo"))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(CurrencyUnit.USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setDiscountFraction(1)
.setDiscountFraction(1.0)
.build());
bulkPricingPackage =
new BulkPricingPackage.Builder()
@@ -206,8 +208,9 @@ public class CheckBulkComplianceActionTest {
.setAllowedTlds(ImmutableSet.of("foo"))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setDiscountFraction(1)
.setDiscountFraction(1.0)
.build());
BulkPricingPackage bulkPricingPackage2 =
new BulkPricingPackage.Builder()
@@ -263,8 +266,9 @@ public class CheckBulkComplianceActionTest {
.setAllowedTlds(ImmutableSet.of("foo"))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setDiscountFraction(1)
.setDiscountFraction(1.0)
.build());
BulkPricingPackage packagePromotion2 =
new BulkPricingPackage.Builder()
@@ -338,8 +342,9 @@ public class CheckBulkComplianceActionTest {
.setAllowedTlds(ImmutableSet.of("foo"))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setDiscountFraction(1)
.setDiscountFraction(1.0)
.build());
BulkPricingPackage bulkPricingPackage2 =
new BulkPricingPackage.Builder()
@@ -404,8 +409,9 @@ public class CheckBulkComplianceActionTest {
.setAllowedTlds(ImmutableSet.of("foo"))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setDiscountFraction(1)
.setDiscountFraction(1.0)
.build());
BulkPricingPackage bulkPricingPackage2 =
new BulkPricingPackage.Builder()
@@ -43,6 +43,7 @@ import google.registry.persistence.transaction.CriteriaQueryBuilder;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.FakeClock;
import java.util.Optional;
import org.apache.beam.sdk.coders.StringUtf8Coder;
import org.apache.beam.sdk.testing.PAssert;
import org.apache.beam.sdk.values.PCollection;
@@ -191,7 +192,7 @@ public class RegistryJpaReadTest {
StatusValue.SERVER_UPDATE_PROHIBITED,
StatusValue.SERVER_RENEW_PROHIBITED,
StatusValue.SERVER_HOLD))
.setRegistrant(contact.createVKey())
.setRegistrant(Optional.of(contact.createVKey()))
.setContacts(ImmutableSet.of())
.setSubordinateHosts(ImmutableSet.of("ns1.example.com"))
.setPersistedCurrentSponsorRegistrarId(registrar.getRegistrarId())
@@ -31,6 +31,7 @@ import static google.registry.rde.RdeResourceType.HOST;
import static google.registry.rde.RdeResourceType.REGISTRAR;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.insertSimpleResources;
import static google.registry.testing.DatabaseHelper.newDomain;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistActiveHost;
@@ -83,10 +84,10 @@ import google.registry.rde.PendingDeposit;
import google.registry.rde.RdeResourceType;
import google.registry.testing.CloudTasksHelper;
import google.registry.testing.CloudTasksHelper.TaskMatcher;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeKeyringModule;
import java.io.IOException;
import java.util.Optional;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -266,23 +267,23 @@ public class RdePipelineTest {
persistHostHistory(host1);
Domain helloDomain =
persistEppResource(
DatabaseHelper.newDomain("hello.soy", contact1)
.asBuilder()
.addNameserver(host1.createVKey())
.build());
newDomain("hello.soy", contact1).asBuilder().addNameserver(host1.createVKey()).build());
persistDomainHistory(helloDomain);
persistHostHistory(persistActiveHost("not-used-subordinate.hello.soy"));
Host host2 = persistActiveHost("ns1.hello.soy");
persistHostHistory(host2);
// This domain has no registrant.
Domain kittyDomain =
persistEppResource(
DatabaseHelper.newDomain("kitty.fun", contact2)
newDomain("kitty.fun", contact2)
.asBuilder()
.addNameservers(ImmutableSet.of(host1.createVKey(), host2.createVKey()))
.setRegistrant(Optional.empty())
.build());
persistDomainHistory(kittyDomain);
// Should not appear because the TLD is not included in a pending deposit.
persistDomainHistory(persistEppResource(DatabaseHelper.newDomain("lol.cat", contact1)));
persistDomainHistory(persistEppResource(newDomain("lol.cat", contact1)));
// To be deleted.
Domain deletedDomain = persistActiveDomain("deleted.soy");
persistDomainHistory(deletedDomain);
@@ -325,7 +326,7 @@ public class RdePipelineTest {
persistHostHistory(futureHost);
persistDomainHistory(
persistEppResource(
DatabaseHelper.newDomain("future.soy", futureContact)
newDomain("future.soy", futureContact)
.asBuilder()
.setNameservers(futureHost.createVKey())
.build()));
@@ -379,18 +380,30 @@ public class RdePipelineTest {
// The same registrars are attached to all the pending deposits.
.containsExactly("New Registrar", "The Registrar", "external_monitoring");
// Domain fragments.
if ("soy".equals(kv.getKey().tld())) {
assertThat(
getFragmentForType(kv, DOMAIN)
.map(getXmlElement(DOMAIN_NAME_PATTERN))
.collect(toImmutableSet()))
.containsExactly("hello.soy");
} else {
assertThat(
getFragmentForType(kv, DOMAIN)
.map(getXmlElement(DOMAIN_NAME_PATTERN))
.collect(toImmutableSet()))
.containsExactly("cat.fun");
ImmutableSet<DepositFragment> domainFrags =
getFragmentForType(kv, DOMAIN).collect(toImmutableSet());
assertThat(domainFrags).hasSize(1);
if ("fun".equals(kv.getKey().tld())) {
// Note that this fragment contains no registrant (which is valid).
assertThat(domainFrags.stream().findFirst().get().xml().strip())
.isEqualTo(
"""
<rdeDomain:domain>
<rdeDomain:name>cat.fun</rdeDomain:name>
<rdeDomain:roid>15-FUN</rdeDomain:roid>
<rdeDomain:uName>cat.fun</rdeDomain:uName>
<rdeDomain:status s="ok"/>
<rdeDomain:contact type="admin">contact456</rdeDomain:contact>
<rdeDomain:contact type="tech">contact456</rdeDomain:contact>
<rdeDomain:ns>
<domain:hostObj>ns1.external.tld</domain:hostObj>
<domain:hostObj>ns1.hello.soy</domain:hostObj>
</rdeDomain:ns>
<rdeDomain:clID>TheRegistrar</rdeDomain:clID>
<rdeDomain:crRr>TheRegistrar</rdeDomain:crRr>
<rdeDomain:crDate>1970-01-01T00:00:00Z</rdeDomain:crDate>
<rdeDomain:exDate>294247-01-10T04:00:54Z</rdeDomain:exDate>
</rdeDomain:domain>""");
}
if (kv.getKey().mode().equals(FULL)) {
// Contact fragments for hello.soy.
@@ -400,12 +413,35 @@ public class RdePipelineTest {
.map(getXmlElement(CONTACT_ID_PATTERN))
.collect(toImmutableSet()))
.containsExactly("contact1234", "contact789");
// Host fragments for hello.soy.
assertThat(
getFragmentForType(kv, HOST)
.map(getXmlElement(HOST_NAME_PATTERN))
.collect(toImmutableSet()))
.containsExactly("ns1.external.tld", "ns1.lol.cat");
// Domain fragments for hello.soy: Note that this contains a registrant.
assertThat(domainFrags.stream().findFirst().get().xml().strip())
.isEqualTo(
"""
<rdeDomain:domain>
<rdeDomain:name>hello.soy</rdeDomain:name>
<rdeDomain:roid>E-SOY</rdeDomain:roid>
<rdeDomain:uName>hello.soy</rdeDomain:uName>
<rdeDomain:status s="ok"/>
<rdeDomain:registrant>contact1234</rdeDomain:registrant>
<rdeDomain:contact type="admin">contact789</rdeDomain:contact>
<rdeDomain:contact type="tech">contact1234</rdeDomain:contact>
<rdeDomain:ns>
<domain:hostObj>ns1.external.tld</domain:hostObj>
<domain:hostObj>ns1.lol.cat</domain:hostObj>
</rdeDomain:ns>
<rdeDomain:clID>TheRegistrar</rdeDomain:clID>
<rdeDomain:crRr>TheRegistrar</rdeDomain:crRr>
<rdeDomain:crDate>1970-01-01T00:00:00Z</rdeDomain:crDate>
<rdeDomain:exDate>294247-01-10T04:00:54Z</rdeDomain:exDate>
</rdeDomain:domain>""");
} else {
// Contact fragments for cat.fun.
assertThat(
@@ -413,6 +449,7 @@ public class RdePipelineTest {
.map(getXmlElement(CONTACT_ID_PATTERN))
.collect(toImmutableSet()))
.containsExactly("contactABC");
// Host fragments for cat.soy.
assertThat(
getFragmentForType(kv, HOST)
@@ -429,6 +466,25 @@ public class RdePipelineTest {
fragment.type().equals(CONTACT)
|| fragment.type().equals(HOST)))
.isFalse();
// Domain fragments for hello.soy: Note that this contains no contact info.
assertThat(domainFrags.stream().findFirst().get().xml().strip())
.isEqualTo(
"""
<rdeDomain:domain>
<rdeDomain:name>hello.soy</rdeDomain:name>
<rdeDomain:roid>E-SOY</rdeDomain:roid>
<rdeDomain:uName>hello.soy</rdeDomain:uName>
<rdeDomain:status s="ok"/>
<rdeDomain:ns>
<domain:hostObj>ns1.external.tld</domain:hostObj>
<domain:hostObj>ns1.lol.cat</domain:hostObj>
</rdeDomain:ns>
<rdeDomain:clID>TheRegistrar</rdeDomain:clID>
<rdeDomain:crRr>TheRegistrar</rdeDomain:crRr>
<rdeDomain:crDate>1970-01-01T00:00:00Z</rdeDomain:crDate>
<rdeDomain:exDate>294247-01-10T04:00:54Z</rdeDomain:exDate>
</rdeDomain:domain>""");
}
});
return null;
@@ -54,6 +54,7 @@ import google.registry.util.Retrier;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import org.apache.beam.sdk.coders.KvCoder;
import org.apache.beam.sdk.coders.SerializableCoder;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
@@ -297,7 +298,7 @@ class Spec11PipelineTest {
.setLastEppUpdateTime(fakeClock.nowUtc())
.setLastEppUpdateRegistrarId(registrar.getRegistrarId())
.setLastTransferTime(fakeClock.nowUtc())
.setRegistrant(contact.createVKey())
.setRegistrant(Optional.of(contact.createVKey()))
.setPersistedCurrentSponsorRegistrarId(registrar.getRegistrarId())
.setRegistrationExpirationTime(fakeClock.nowUtc().plusYears(1))
.setAuthInfo(DomainAuthInfo.create(PasswordAuth.create("password")))
@@ -1371,6 +1371,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
.setTokenType(SINGLE_USE)
.setDomainName("resdom.tld")
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.build());
// Despite the domain being FULLY_BLOCKED, the non-superuser create succeeds the domain is also
// RESERVED_FOR_SPECIFIC_USE and the correct allocation token is passed.
@@ -3413,6 +3414,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.build());
assertThat(
DomainCreateFlow.getRenewalPriceInfo(
@@ -3439,6 +3441,7 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.build())),
new FeesAndCredits.Builder()
.setCurrency(USD)
@@ -3519,6 +3522,22 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
assertSuccessfulCreate("tld", ImmutableSet.of(), token);
}
@Test
void testSuccess_nonpremiumCreateToken() throws Exception {
createTld("example");
persistContactsAndHosts();
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setRegistrationBehavior(RegistrationBehavior.NONPREMIUM_CREATE)
.setDomainName("rich.example")
.build());
setEppInput(
"domain_create_premium_allocationtoken.xml", ImmutableMap.of("YEARS", "1", "FEE", "13.00"));
runFlowAssertResponse(loadFile("domain_create_nonpremium_token_response.xml"));
}
@Test
void testFailure_quietPeriod_defaultTokenPresent() throws Exception {
persistResource(
@@ -3875,10 +3894,12 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(BULK_PRICING)
.setDiscountFraction(1.0)
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setAllowedTlds(ImmutableSet.of("tld"))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.build());
persistContactsAndHosts();
setEppInput(
@@ -3903,10 +3924,12 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(BULK_PRICING)
.setDiscountFraction(1.0)
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setAllowedTlds(ImmutableSet.of("tld"))
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.build());
persistContactsAndHosts();
setEppInput(
@@ -103,6 +103,7 @@ import google.registry.model.transfer.TransferStatus;
import google.registry.testing.CloudTasksHelper.TaskMatcher;
import google.registry.testing.DatabaseHelper;
import java.util.Map;
import java.util.Optional;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.joda.time.Duration;
@@ -162,7 +163,7 @@ class DomainDeleteFlowTest extends ResourceFlowTestCase<DomainDeleteFlow, Domain
DatabaseHelper.newDomain(getUniqueIdFromCommand())
.asBuilder()
.setCreationTimeForTest(TIME_BEFORE_FLOW)
.setRegistrant(contact.createVKey())
.setRegistrant(Optional.of(contact.createVKey()))
.setRegistrationExpirationTime(expirationTime)
.build());
earlierHistoryEntry =
@@ -738,7 +739,8 @@ class DomainDeleteFlowTest extends ResourceFlowTestCase<DomainDeleteFlow, Domain
DatabaseHelper.newDomain("example1.tld")
.asBuilder()
.setRegistrant(
loadByForeignKey(Contact.class, "sh8013", clock.nowUtc()).get().createVKey())
Optional.of(
loadByForeignKey(Contact.class, "sh8013", clock.nowUtc()).get().createVKey()))
.setNameservers(ImmutableSet.of(host.createVKey()))
.setDeletionTime(START_OF_TIME)
.build());
@@ -78,6 +78,7 @@ import google.registry.persistence.VKey;
import google.registry.persistence.transaction.JpaTransactionManagerExtension;
import google.registry.testing.DatabaseHelper;
import google.registry.xml.ValidationMode;
import java.util.Optional;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import org.joda.money.Money;
@@ -139,7 +140,7 @@ 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(registrant.createVKey())
.setRegistrant(Optional.of(registrant.createVKey()))
.setContacts(
ImmutableSet.of(
DesignatedContact.create(Type.ADMIN, contact.createVKey()),
@@ -227,6 +228,26 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
doSuccessfulTest("domain_info_response.xml");
}
@Test
void testSuccess_noRegistrant() throws Exception {
persistTestEntities(false);
domain = persistResource(domain.asBuilder().setRegistrant(Optional.empty()).build());
doSuccessfulTest("domain_info_response_no_registrant.xml", false);
}
@Test
void testSuccess_noContacts() throws Exception {
persistTestEntities(false);
domain =
persistResource(
domain
.asBuilder()
.setRegistrant(Optional.empty())
.setContacts(ImmutableSet.of())
.build());
doSuccessfulTest("domain_info_response_no_contacts.xml", false);
}
@Test
void testSuccess_clTridNotSpecified() throws Exception {
setEppInput("domain_info_no_cltrid.xml");
@@ -1097,8 +1118,9 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
.setAllowedTlds(ImmutableSet.of("foo"))
.setAllowedRegistrarIds(ImmutableSet.of("NewRegistrar"))
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setDiscountFraction(1)
.setDiscountFraction(1.0)
.build());
domain = domain.asBuilder().setCurrentBulkToken(token.createVKey()).build();
persistResource(domain);
@@ -1118,8 +1140,9 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
.setAllowedTlds(ImmutableSet.of("foo"))
.setAllowedRegistrarIds(ImmutableSet.of("NewRegistrar"))
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setDiscountFraction(1)
.setDiscountFraction(1.0)
.build());
domain = domain.asBuilder().setCurrentBulkToken(token.createVKey()).build();
persistResource(domain);
@@ -1151,8 +1174,9 @@ class DomainInfoFlowTest extends ResourceFlowTestCase<DomainInfoFlow, Domain> {
.setAllowedTlds(ImmutableSet.of("foo"))
.setAllowedRegistrarIds(ImmutableSet.of("NewRegistrar"))
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setDiscountFraction(1)
.setDiscountFraction(1.0)
.build());
domain = domain.asBuilder().setCurrentBulkToken(token.createVKey()).build();
persistResource(domain);
@@ -55,6 +55,7 @@ import google.registry.testing.DatabaseHelper;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeHttpSession;
import google.registry.util.Clock;
import java.math.BigDecimal;
import java.util.Optional;
import org.joda.money.Money;
import org.joda.time.DateTime;
@@ -146,7 +147,7 @@ public class DomainPricingLogicTest {
new FeesAndCredits.Builder()
.setCurrency(USD)
// (13 + 11) * 0.85 == 20.40
.addFeeOrCredit(Fee.create(Money.of(USD, 20.4).getAmount(), CREATE, false))
.addFeeOrCredit(Fee.create(new BigDecimal("20.40"), CREATE, false))
.build());
}
@@ -159,7 +160,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 10).getAmount(), RENEW, false))
.addFeeOrCredit(Fee.create(new BigDecimal("10.00"), RENEW, false))
.build());
}
@@ -172,7 +173,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 50).getAmount(), RENEW, false))
.addFeeOrCredit(Fee.create(new BigDecimal("50.00"), RENEW, false))
.build());
}
@@ -185,7 +186,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 100).getAmount(), RENEW, true))
.addFeeOrCredit(Fee.create(new BigDecimal("100.00"), RENEW, true))
.build());
}
@@ -198,7 +199,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 500).getAmount(), RENEW, true))
.addFeeOrCredit(Fee.create(new BigDecimal("500.00"), RENEW, true))
.build());
}
@@ -215,7 +216,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 100).getAmount(), RENEW, true))
.addFeeOrCredit(Fee.create(new BigDecimal("100.00"), RENEW, true))
.build());
}
@@ -241,7 +242,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 50).getAmount(), RENEW, true))
.addFeeOrCredit(Fee.create(new BigDecimal("50.00"), RENEW, true))
.build());
}
@@ -281,7 +282,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 500).getAmount(), RENEW, true))
.addFeeOrCredit(Fee.create(new BigDecimal("500.00"), RENEW, true))
.build());
}
@@ -308,7 +309,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 400).getAmount(), RENEW, true))
.addFeeOrCredit(Fee.create(new BigDecimal("400.00"), RENEW, true))
.build());
}
@@ -350,7 +351,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 10).getAmount(), RENEW, false))
.addFeeOrCredit(Fee.create(new BigDecimal("10.00"), RENEW, false))
.build());
}
@@ -376,7 +377,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 5).getAmount(), RENEW, false))
.addFeeOrCredit(Fee.create(new BigDecimal("5.00"), RENEW, false))
.build());
}
@@ -394,7 +395,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 50).getAmount(), RENEW, false))
.addFeeOrCredit(Fee.create(new BigDecimal("50.00"), RENEW, false))
.build());
}
@@ -421,7 +422,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 40).getAmount(), RENEW, false))
.addFeeOrCredit(Fee.create(new BigDecimal("40.00"), RENEW, false))
.build());
}
@@ -439,7 +440,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 10).getAmount(), RENEW, false))
.addFeeOrCredit(Fee.create(new BigDecimal("10.00"), RENEW, false))
.build());
}
@@ -466,7 +467,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 5).getAmount(), RENEW, false))
.addFeeOrCredit(Fee.create(new BigDecimal("5.00"), RENEW, false))
.build());
}
@@ -484,7 +485,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 50).getAmount(), RENEW, false))
.addFeeOrCredit(Fee.create(new BigDecimal("50.00"), RENEW, false))
.build());
}
@@ -512,7 +513,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 40).getAmount(), RENEW, false))
.addFeeOrCredit(Fee.create(new BigDecimal("40.00"), RENEW, false))
.build());
}
@@ -530,7 +531,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 10).getAmount(), RENEW, false))
.addFeeOrCredit(Fee.create(new BigDecimal("10.00"), RENEW, false))
.build());
}
@@ -548,7 +549,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 50).getAmount(), RENEW, false))
.addFeeOrCredit(Fee.create(new BigDecimal("50.00"), RENEW, false))
.build());
}
@@ -567,7 +568,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 1).getAmount(), RENEW, false))
.addFeeOrCredit(Fee.create(new BigDecimal("1.00"), RENEW, false))
.build());
}
@@ -597,7 +598,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 1).getAmount(), RENEW, false))
.addFeeOrCredit(Fee.create(new BigDecimal("1.00"), RENEW, false))
.build());
}
@@ -628,7 +629,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 1).getAmount(), RENEW, false))
.addFeeOrCredit(Fee.create(new BigDecimal("1.00"), RENEW, false))
.build());
assertThat(
Iterables.getLast(DatabaseHelper.loadAllOf(BillingRecurrence.class))
@@ -651,7 +652,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 5).getAmount(), RENEW, false))
.addFeeOrCredit(Fee.create(new BigDecimal("5.00"), RENEW, false))
.build());
}
@@ -679,7 +680,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 5).getAmount(), RENEW, false))
.addFeeOrCredit(Fee.create(new BigDecimal("5.00"), RENEW, false))
.build());
}
@@ -698,7 +699,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 17).getAmount(), RENEW, false))
.addFeeOrCredit(Fee.create(new BigDecimal("17.00"), RENEW, false))
.build());
}
@@ -717,7 +718,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 85).getAmount(), RENEW, false))
.addFeeOrCredit(Fee.create(new BigDecimal("85.00"), RENEW, false))
.build());
}
@@ -739,7 +740,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 10).getAmount(), RENEW, false))
.addFeeOrCredit(Fee.create(new BigDecimal("10.00"), RENEW, false))
.build());
}
@@ -750,7 +751,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 100).getAmount(), RENEW, true))
.addFeeOrCredit(Fee.create(new BigDecimal("100.00"), RENEW, true))
.build());
}
@@ -765,7 +766,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 10).getAmount(), RENEW, false))
.addFeeOrCredit(Fee.create(new BigDecimal("10.00"), RENEW, false))
.build());
}
@@ -780,7 +781,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 100).getAmount(), RENEW, true))
.addFeeOrCredit(Fee.create(new BigDecimal("100.00"), RENEW, true))
.build());
}
@@ -796,7 +797,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 10).getAmount(), RENEW, false))
.addFeeOrCredit(Fee.create(new BigDecimal("10.00"), RENEW, false))
.build());
}
@@ -812,7 +813,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 10).getAmount(), RENEW, false))
.addFeeOrCredit(Fee.create(new BigDecimal("10.00"), RENEW, false))
.build());
}
@@ -829,7 +830,7 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 1.23).getAmount(), RENEW, false))
.addFeeOrCredit(Fee.create(new BigDecimal("1.23"), RENEW, false))
.build());
}
@@ -846,7 +847,61 @@ public class DomainPricingLogicTest {
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 1.23).getAmount(), RENEW, false))
.addFeeOrCredit(Fee.create(new BigDecimal("1.23"), RENEW, false))
.build());
}
@Test
void testGetDomainCreatePrice_nonPremiumCreate_unaffectedRenewal() throws EppException {
AllocationToken allocationToken =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("premium.example")
.setRegistrationBehavior(AllocationToken.RegistrationBehavior.NONPREMIUM_CREATE)
.build());
assertThat(
domainPricingLogic.getCreatePrice(
tld,
"premium.example",
clock.nowUtc(),
1,
false,
false,
Optional.of(allocationToken)))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(new BigDecimal("13.00"), CREATE, false))
.build());
// Two-year create should be 13 (standard price) + 100 (premium price)
assertThat(
domainPricingLogic.getCreatePrice(
tld,
"premium.example",
clock.nowUtc(),
2,
false,
false,
Optional.of(allocationToken)))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(new BigDecimal("113.00"), CREATE, false))
.build());
assertThat(
domainPricingLogic.getRenewPrice(
tld,
"premium.example",
clock.nowUtc(),
1,
persistDomainAndSetRecurrence("premium.example", DEFAULT, Optional.empty()),
Optional.of(allocationToken)))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(new BigDecimal("100.00"), RENEW, true))
.build());
}
}
@@ -1257,9 +1257,11 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(BULK_PRICING)
.setDiscountFraction(1.0)
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setAllowedTlds(ImmutableSet.of("tld"))
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.build());
persistDomain();
@@ -1290,9 +1292,11 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(BULK_PRICING)
.setDiscountFraction(1.0)
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setAllowedTlds(ImmutableSet.of("tld"))
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.build());
persistDomain();
@@ -1327,9 +1331,11 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(BULK_PRICING)
.setDiscountFraction(1.0)
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setAllowedTlds(ImmutableSet.of("tld"))
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.build());
persistDomain(SPECIFIED, Money.of(USD, 2));
@@ -1357,9 +1363,11 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, Domain>
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(BULK_PRICING)
.setDiscountFraction(1.0)
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setAllowedTlds(ImmutableSet.of("tld"))
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.build());
persistDomain(SPECIFIED, Money.of(USD, 2));
@@ -388,7 +388,9 @@ class DomainTransferApproveFlowTest
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(BULK_PRICING)
.setDiscountFraction(1.0)
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.build());
@@ -417,7 +419,9 @@ class DomainTransferApproveFlowTest
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(BULK_PRICING)
.setDiscountFraction(1.0)
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.build());
@@ -1339,7 +1339,9 @@ class DomainTransferRequestFlowTest
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(BULK_PRICING)
.setDiscountFraction(1.0)
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.build());
@@ -1398,7 +1400,9 @@ class DomainTransferRequestFlowTest
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(BULK_PRICING)
.setDiscountFraction(1.0)
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.build());
@@ -1456,7 +1460,9 @@ class DomainTransferRequestFlowTest
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(BULK_PRICING)
.setDiscountFraction(1.0)
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.build());
@@ -162,7 +162,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
DesignatedContact.create(Type.TECH, mak21Contact.createVKey()),
DesignatedContact.create(Type.ADMIN, mak21Contact.createVKey()),
DesignatedContact.create(Type.BILLING, mak21Contact.createVKey())))
.setRegistrant(mak21Contact.createVKey())
.setRegistrant(Optional.of(mak21Contact.createVKey()))
.setNameservers(ImmutableSet.of(host.createVKey()))
.build());
persistResource(
@@ -338,7 +338,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
.asBuilder()
.setNameservers(nameservers.build())
.setContacts(ImmutableSet.copyOf(contacts.subList(0, 3)))
.setRegistrant(contacts.get(3).getContactKey())
.setRegistrant(Optional.of(contacts.get(3).getContactKey()))
.build());
clock.advanceOneMilli();
assertMutatingFlow(true);
@@ -348,7 +348,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
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()).getContactId()).isEqualTo("max_test_7");
assertThat(loadByKey(domain.getRegistrant().get()).getContactId()).isEqualTo("max_test_7");
assertNoBillingEvents();
assertDomainDnsRequests("example.tld");
}
@@ -426,7 +426,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
persistResource(
DatabaseHelper.newDomain(getUniqueIdFromCommand())
.asBuilder()
.setRegistrant(sh8013.createVKey())
.setRegistrant(Optional.of(sh8013.createVKey()))
.build());
clock.advanceOneMilli();
runFlowAssertResponse(loadFile("generic_success_response.xml"));
@@ -441,7 +441,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
persistResource(
DatabaseHelper.newDomain(getUniqueIdFromCommand())
.asBuilder()
.setRegistrant(sh8013Key)
.setRegistrant(Optional.of(sh8013Key))
.setContacts(
ImmutableSet.of(
DesignatedContact.create(Type.ADMIN, sh8013Key),
@@ -1590,7 +1590,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
.setAllowedFullyQualifiedHostNames(ImmutableSet.of("ns1.example.foo"))
.build());
runFlow();
assertThat(loadByKey(reloadResourceByForeignKey().getRegistrant()).getContactId())
assertThat(loadByKey(reloadResourceByForeignKey().getRegistrant().get()).getContactId())
.isEqualTo("sh8013");
}
@@ -1605,7 +1605,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
.forEach(
contact ->
assertThat(loadByKey(contact.getContactKey()).getContactId()).isEqualTo("mak21"));
assertThat(loadByKey(reloadResourceByForeignKey().getRegistrant()).getContactId())
assertThat(loadByKey(reloadResourceByForeignKey().getRegistrant().get()).getContactId())
.isEqualTo("mak21");
runFlow();
@@ -1615,7 +1615,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
.forEach(
contact ->
assertThat(loadByKey(contact.getContactKey()).getContactId()).isEqualTo("sh8013"));
assertThat(loadByKey(reloadResourceByForeignKey().getRegistrant()).getContactId())
assertThat(loadByKey(reloadResourceByForeignKey().getRegistrant().get()).getContactId())
.isEqualTo("sh8013");
}
@@ -15,6 +15,7 @@
package google.registry.model;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.console.User.IAP_SECURED_WEB_APP_USER_ROLE;
import static google.registry.model.tld.Tld.TldState.GENERAL_AVAILABILITY;
import static google.registry.model.tld.Tld.TldState.START_DATE_SUNRISE;
import static google.registry.persistence.transaction.JpaTransactionManagerExtension.makeRegistrar1;
@@ -26,16 +27,28 @@ import static google.registry.testing.DatabaseHelper.persistSimpleResource;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.joda.money.CurrencyUnit.USD;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import com.google.cloud.tasks.v2.HttpMethod;
import com.google.common.collect.ImmutableList;
import google.registry.batch.CloudTasksUtils;
import google.registry.model.console.RegistrarRole;
import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.model.tld.Tld;
import google.registry.model.tld.Tld.TldState;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.CloudTasksHelper;
import google.registry.testing.CloudTasksHelper.TaskMatcher;
import google.registry.tools.IamClient;
import google.registry.util.CidrAddressBlock;
import google.registry.util.SystemClock;
import java.util.Optional;
import javax.annotation.Nullable;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
@@ -50,6 +63,9 @@ public final class OteAccountBuilderTest {
final JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
private final CloudTasksHelper cloudTasksHelper = new CloudTasksHelper();
private final IamClient iamClient = mock(IamClient.class);
@Test
void testGetRegistrarToTldMap() {
assertThat(OteAccountBuilder.forRegistrarId("myclientid").getRegistrarIdToTldMap())
@@ -89,23 +105,63 @@ public final class OteAccountBuilderTest {
assertThat(registrar.getAllowedTlds()).containsExactly(tld);
}
private static void assertContactExists(String registrarId, String email) {
Registrar registrar = Registrar.loadByRegistrarId(registrarId).get();
assertThat(registrar.getContacts().stream().map(RegistrarPoc::getEmailAddress)).contains(email);
RegistrarPoc contact =
registrar.getContacts().stream()
.filter(c -> email.equals(c.getEmailAddress()))
.findAny()
.get();
assertThat(contact.getEmailAddress()).isEqualTo(email);
assertThat(contact.getLoginEmailAddress()).isEqualTo(email);
public static void verifyUser(String registrarId, String email) {
Optional<User> maybeUser = UserDao.loadUser(email);
assertThat(maybeUser).isPresent();
assertThat(maybeUser.get().getUserRoles().getRegistrarRoles().get(registrarId))
.isEqualTo(RegistrarRole.ACCOUNT_MANAGER);
}
public static void verifyIapPermission(
@Nullable String emailAddress,
Optional<String> maybeGroupEmailAddress,
CloudTasksHelper cloudTasksHelper,
IamClient iamClient) {
if (emailAddress == null) {
cloudTasksHelper.assertNoTasksEnqueued("console-user-group-update");
verifyNoInteractions(iamClient);
} else {
String groupEmailAddress = maybeGroupEmailAddress.orElse(null);
if (groupEmailAddress == null) {
cloudTasksHelper.assertNoTasksEnqueued("console-user-group-update");
verify(iamClient).addBinding(emailAddress, IAP_SECURED_WEB_APP_USER_ROLE);
} else {
cloudTasksHelper.assertTasksEnqueued(
"console-user-group-update",
new TaskMatcher()
.service("TOOLS")
.method(HttpMethod.POST)
.path("/_dr/admin/updateUserGroup")
.param("userEmailAddress", emailAddress)
.param("groupEmailAddress", groupEmailAddress)
.param("groupUpdateMode", "ADD"));
verifyNoInteractions(iamClient);
}
}
}
@Test
void testUpdateUserGroup() {
CloudTasksUtils cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils();
OteAccountBuilder.forRegistrarId("myclientid")
.addUser("email@example.com")
.grantIapPermission(Optional.of("console@example.com"), cloudTasksUtils, iamClient);
verifyIapPermission(
"email@example.com", Optional.of("console@example.com"), cloudTasksHelper, iamClient);
}
@Test
void testGrantIndividualPermission() {
CloudTasksUtils cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils();
OteAccountBuilder.forRegistrarId("myclientid")
.addUser("email@example.com")
.grantIapPermission(Optional.empty(), cloudTasksUtils, iamClient);
verifyIapPermission("email@example.com", Optional.empty(), cloudTasksHelper, iamClient);
}
@Test
void testCreateOteEntities_success() {
OteAccountBuilder.forRegistrarId("myclientid")
.addContact("email@example.com")
.buildAndPersist();
OteAccountBuilder.forRegistrarId("myclientid").addUser("email@example.com").buildAndPersist();
assertTldExists("myclientid-sunrise", START_DATE_SUNRISE, Money.zero(USD));
assertTldExists("myclientid-ga", GENERAL_AVAILABILITY, Money.zero(USD));
@@ -114,18 +170,18 @@ public final class OteAccountBuilderTest {
assertRegistrarExists("myclientid-3", "myclientid-ga");
assertRegistrarExists("myclientid-4", "myclientid-ga");
assertRegistrarExists("myclientid-5", "myclientid-eap");
assertContactExists("myclientid-1", "email@example.com");
assertContactExists("myclientid-3", "email@example.com");
assertContactExists("myclientid-4", "email@example.com");
assertContactExists("myclientid-5", "email@example.com");
verifyUser("myclientid-1", "email@example.com");
verifyUser("myclientid-3", "email@example.com");
verifyUser("myclientid-4", "email@example.com");
verifyUser("myclientid-5", "email@example.com");
}
@Test
void testCreateOteEntities_multipleContacts_success() {
OteAccountBuilder.forRegistrarId("myclientid")
.addContact("email@example.com")
.addContact("other@example.com")
.addContact("someone@example.com")
.addUser("email@example.com")
.addUser("other@example.com")
.addUser("someone@example.com")
.buildAndPersist();
assertTldExists("myclientid-sunrise", START_DATE_SUNRISE, Money.zero(USD));
@@ -135,18 +191,18 @@ public final class OteAccountBuilderTest {
assertRegistrarExists("myclientid-3", "myclientid-ga");
assertRegistrarExists("myclientid-4", "myclientid-ga");
assertRegistrarExists("myclientid-5", "myclientid-eap");
assertContactExists("myclientid-1", "email@example.com");
assertContactExists("myclientid-3", "email@example.com");
assertContactExists("myclientid-4", "email@example.com");
assertContactExists("myclientid-5", "email@example.com");
assertContactExists("myclientid-1", "other@example.com");
assertContactExists("myclientid-3", "other@example.com");
assertContactExists("myclientid-4", "other@example.com");
assertContactExists("myclientid-5", "other@example.com");
assertContactExists("myclientid-1", "someone@example.com");
assertContactExists("myclientid-3", "someone@example.com");
assertContactExists("myclientid-4", "someone@example.com");
assertContactExists("myclientid-5", "someone@example.com");
verifyUser("myclientid-1", "email@example.com");
verifyUser("myclientid-3", "email@example.com");
verifyUser("myclientid-4", "email@example.com");
verifyUser("myclientid-5", "email@example.com");
verifyUser("myclientid-1", "other@example.com");
verifyUser("myclientid-3", "other@example.com");
verifyUser("myclientid-4", "other@example.com");
verifyUser("myclientid-5", "other@example.com");
verifyUser("myclientid-1", "someone@example.com");
verifyUser("myclientid-3", "someone@example.com");
verifyUser("myclientid-4", "someone@example.com");
verifyUser("myclientid-5", "someone@example.com");
}
@Test
@@ -223,7 +279,7 @@ public final class OteAccountBuilderTest {
OteAccountBuilder oteSetupHelper = OteAccountBuilder.forRegistrarId("myclientid");
IllegalStateException thrown =
assertThrows(IllegalStateException.class, () -> oteSetupHelper.buildAndPersist());
assertThrows(IllegalStateException.class, oteSetupHelper::buildAndPersist);
assertThat(thrown)
.hasMessageThat()
.contains("Found existing object(s) conflicting with OT&E objects");
@@ -236,7 +292,7 @@ public final class OteAccountBuilderTest {
OteAccountBuilder oteSetupHelper = OteAccountBuilder.forRegistrarId("myclientid");
IllegalStateException thrown =
assertThrows(IllegalStateException.class, () -> oteSetupHelper.buildAndPersist());
assertThrows(IllegalStateException.class, oteSetupHelper::buildAndPersist);
assertThat(thrown)
.hasMessageThat()
.contains("Found existing object(s) conflicting with OT&E objects");
@@ -261,7 +317,7 @@ public final class OteAccountBuilderTest {
void testCreateOteEntities_doubleCreation_actuallyReplaces() {
OteAccountBuilder.forRegistrarId("myclientid")
.setPassword("oldPassword")
.addContact("email@example.com")
.addUser("email@example.com")
.buildAndPersist();
assertThat(Registrar.loadByRegistrarId("myclientid-3").get().verifyPassword("oldPassword"))
@@ -269,7 +325,7 @@ public final class OteAccountBuilderTest {
OteAccountBuilder.forRegistrarId("myclientid")
.setPassword("newPassword")
.addContact("email@example.com")
.addUser("email@example.com")
.setReplaceExisting(true)
.buildAndPersist();
@@ -281,19 +337,17 @@ public final class OteAccountBuilderTest {
@Test
void testCreateOteEntities_doubleCreation_keepsOldContacts() {
OteAccountBuilder.forRegistrarId("myclientid")
.addContact("email@example.com")
.buildAndPersist();
OteAccountBuilder.forRegistrarId("myclientid").addUser("email@example.com").buildAndPersist();
assertContactExists("myclientid-3", "email@example.com");
verifyUser("myclientid-3", "email@example.com");
OteAccountBuilder.forRegistrarId("myclientid")
.addContact("other@example.com")
.addUser("other@example.com")
.setReplaceExisting(true)
.buildAndPersist();
assertContactExists("myclientid-3", "other@example.com");
assertContactExists("myclientid-3", "email@example.com");
verifyUser("myclientid-3", "other@example.com");
verifyUser("myclientid-3", "email@example.com");
}
@Test
@@ -79,9 +79,7 @@ public final class OteStatsTestHelper {
public static void setupIncompleteOte(String baseClientId) throws IOException {
createTld("tld");
persistPremiumList("default_sandbox_list", USD, "sandbox,USD 1000");
OteAccountBuilder.forRegistrarId(baseClientId)
.addContact("email@example.com")
.buildAndPersist();
OteAccountBuilder.forRegistrarId(baseClientId).buildAndPersist();
String oteAccount1 = String.format("%s-1", baseClientId);
DateTime now = DateTime.now(DateTimeZone.UTC);
persistResource(
@@ -15,6 +15,9 @@
package google.registry.model.common;
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.FeatureName.TEST_FEATURE;
import static google.registry.model.common.FeatureFlag.FeatureStatus.ACTIVE;
import static google.registry.model.common.FeatureFlag.FeatureStatus.INACTIVE;
import static google.registry.testing.DatabaseHelper.loadByEntity;
@@ -42,8 +45,8 @@ public class FeatureFlagTest extends EntityTestCase {
void testSuccess_persistence() {
FeatureFlag featureFlag =
new FeatureFlag.Builder()
.setFeatureName("testFlag")
.setStatus(
.setFeatureName(TEST_FEATURE)
.setStatusMap(
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
.put(START_OF_TIME, INACTIVE)
.put(DateTime.now(UTC).plusWeeks(8), ACTIVE)
@@ -58,15 +61,15 @@ public class FeatureFlagTest extends EntityTestCase {
void testSuccess_getSingleFlag() {
FeatureFlag featureFlag =
new FeatureFlag.Builder()
.setFeatureName("testFlag")
.setStatus(
.setFeatureName(TEST_FEATURE)
.setStatusMap(
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
.put(START_OF_TIME, INACTIVE)
.put(DateTime.now(UTC).plusWeeks(8), ACTIVE)
.build())
.build();
persistResource(featureFlag);
assertThat(FeatureFlag.get("testFlag")).isEqualTo(featureFlag);
assertThat(FeatureFlag.get(TEST_FEATURE)).isEqualTo(featureFlag);
}
@Test
@@ -74,8 +77,8 @@ public class FeatureFlagTest extends EntityTestCase {
FeatureFlag featureFlag1 =
persistResource(
new FeatureFlag.Builder()
.setFeatureName("testFlag1")
.setStatus(
.setFeatureName(TEST_FEATURE)
.setStatusMap(
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
.put(START_OF_TIME, INACTIVE)
.put(DateTime.now(UTC).plusWeeks(8), ACTIVE)
@@ -84,8 +87,8 @@ public class FeatureFlagTest extends EntityTestCase {
FeatureFlag featureFlag2 =
persistResource(
new FeatureFlag.Builder()
.setFeatureName("testFlag2")
.setStatus(
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
.setStatusMap(
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
.put(START_OF_TIME, INACTIVE)
.put(DateTime.now(UTC).plusWeeks(3), INACTIVE)
@@ -94,14 +97,18 @@ public class FeatureFlagTest extends EntityTestCase {
FeatureFlag featureFlag3 =
persistResource(
new FeatureFlag.Builder()
.setFeatureName("testFlag3")
.setStatus(
.setFeatureName(MINIMUM_DATASET_CONTACTS_PROHIBITED)
.setStatusMap(
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
.put(START_OF_TIME, INACTIVE)
.build())
.build());
ImmutableSet<FeatureFlag> featureFlags =
FeatureFlag.get(ImmutableSet.of("testFlag1", "testFlag2", "testFlag3"));
FeatureFlag.getAll(
ImmutableSet.of(
TEST_FEATURE,
MINIMUM_DATASET_CONTACTS_OPTIONAL,
MINIMUM_DATASET_CONTACTS_PROHIBITED));
assertThat(featureFlags.size()).isEqualTo(3);
assertThat(featureFlags).containsExactly(featureFlag1, featureFlag2, featureFlag3);
}
@@ -110,8 +117,8 @@ public class FeatureFlagTest extends EntityTestCase {
void testFailure_getMultipleFlagsOneMissing() {
persistResource(
new FeatureFlag.Builder()
.setFeatureName("testFlag1")
.setStatus(
.setFeatureName(TEST_FEATURE)
.setStatusMap(
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
.put(START_OF_TIME, INACTIVE)
.put(DateTime.now(UTC).plusWeeks(8), ACTIVE)
@@ -119,8 +126,8 @@ public class FeatureFlagTest extends EntityTestCase {
.build());
persistResource(
new FeatureFlag.Builder()
.setFeatureName("testFlag2")
.setStatus(
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
.setStatusMap(
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
.put(START_OF_TIME, INACTIVE)
.put(DateTime.now(UTC).plusWeeks(3), INACTIVE)
@@ -129,74 +136,48 @@ public class FeatureFlagTest extends EntityTestCase {
FeatureFlagNotFoundException thrown =
assertThrows(
FeatureFlagNotFoundException.class,
() -> FeatureFlag.get(ImmutableSet.of("missingFlag", "testFlag1", "testFlag2")));
() ->
FeatureFlag.getAll(
ImmutableSet.of(
TEST_FEATURE,
MINIMUM_DATASET_CONTACTS_OPTIONAL,
MINIMUM_DATASET_CONTACTS_PROHIBITED)));
assertThat(thrown)
.hasMessageThat()
.isEqualTo("No feature flag object(s) found for missingFlag");
.isEqualTo("No feature flag object(s) found for MINIMUM_DATASET_CONTACTS_PROHIBITED");
}
@Test
void testFailure_featureFlagNotPresent() {
FeatureFlagNotFoundException thrown =
assertThrows(FeatureFlagNotFoundException.class, () -> FeatureFlag.get("fakeFlag"));
assertThat(thrown).hasMessageThat().isEqualTo("No feature flag object(s) found for fakeFlag");
}
@Test
void testFailure_resetFeatureName() {
FeatureFlag featureFlag =
new FeatureFlag.Builder()
.setFeatureName("testFlag")
.setStatus(
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
.put(START_OF_TIME, INACTIVE)
.put(DateTime.now(UTC).plusWeeks(8), ACTIVE)
.build())
.build();
IllegalStateException thrown =
assertThrows(
IllegalStateException.class,
() -> featureFlag.asBuilder().setFeatureName("differentName"));
assertThat(thrown).hasMessageThat().isEqualTo("Feature name can only be set once");
assertThrows(FeatureFlagNotFoundException.class, () -> FeatureFlag.get(TEST_FEATURE));
assertThat(thrown)
.hasMessageThat()
.isEqualTo("No feature flag object(s) found for TEST_FEATURE");
}
@Test
void testFailure_nullFeatureName() {
FeatureFlag.Builder featureFlagBuilder =
new FeatureFlag.Builder()
.setStatus(
.setStatusMap(
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
.put(START_OF_TIME, INACTIVE)
.put(DateTime.now(UTC).plusWeeks(8), ACTIVE)
.build());
IllegalArgumentException thrown =
assertThrows(IllegalArgumentException.class, () -> featureFlagBuilder.build());
assertThat(thrown).hasMessageThat().isEqualTo("Feature name must not be null or empty");
}
@Test
void testFailure_emptyFeatureName() {
FeatureFlag.Builder featureFlagBuilder =
new FeatureFlag.Builder()
.setFeatureName("")
.setStatus(
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
.put(START_OF_TIME, INACTIVE)
.put(DateTime.now(UTC).plusWeeks(8), ACTIVE)
.build());
IllegalArgumentException thrown =
assertThrows(IllegalArgumentException.class, () -> featureFlagBuilder.build());
assertThat(thrown).hasMessageThat().isEqualTo("Feature name must not be null or empty");
assertThat(thrown).hasMessageThat().isEqualTo("FeatureName cannot be null");
}
@Test
void testFailure_invalidStatusMap() {
FeatureFlag.Builder featureFlagBuilder = new FeatureFlag.Builder().setFeatureName("testFlag");
FeatureFlag.Builder featureFlagBuilder = new FeatureFlag.Builder().setFeatureName(TEST_FEATURE);
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
featureFlagBuilder.setStatus(
featureFlagBuilder.setStatusMap(
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
.put(DateTime.now(UTC).plusWeeks(8), ACTIVE)
.build()));
@@ -16,11 +16,21 @@ package google.registry.model.console;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
import static google.registry.model.console.User.IAP_SECURED_WEB_APP_USER_ROLE;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import com.google.cloud.tasks.v2.HttpMethod;
import google.registry.batch.CloudTasksUtils;
import google.registry.model.EntityTestCase;
import google.registry.testing.CloudTasksHelper;
import google.registry.testing.CloudTasksHelper.TaskMatcher;
import google.registry.testing.DatabaseHelper;
import google.registry.tools.IamClient;
import java.util.Optional;
import org.junit.jupiter.api.Test;
/** Tests for {@link User}. */
@@ -104,4 +114,56 @@ public class UserTest extends EntityTestCase {
assertThat(user.hasRegistryLockPassword()).isFalse();
assertThat(user.verifyRegistryLockPassword("foobar")).isFalse();
}
@Test
void testGrantIapPermission() {
CloudTasksHelper cloudTasksHelper = new CloudTasksHelper();
IamClient iamClient = mock(IamClient.class);
CloudTasksUtils cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils();
// Individual permission.
User.grantIapPermission("email@example.com", Optional.empty(), cloudTasksUtils, iamClient);
cloudTasksHelper.assertNoTasksEnqueued();
verify(iamClient).addBinding("email@example.com", IAP_SECURED_WEB_APP_USER_ROLE);
// Group membership.
User.grantIapPermission(
"email@example.com", Optional.of("console@example.com"), cloudTasksUtils, iamClient);
cloudTasksHelper.assertTasksEnqueued(
"console-user-group-update",
new TaskMatcher()
.service("TOOLS")
.method(HttpMethod.POST)
.path("/_dr/admin/updateUserGroup")
.param("userEmailAddress", "email@example.com")
.param("groupEmailAddress", "console@example.com")
.param("groupUpdateMode", "ADD"));
verifyNoMoreInteractions(iamClient);
}
@Test
void testRevokeIapPermission() {
CloudTasksHelper cloudTasksHelper = new CloudTasksHelper();
IamClient iamClient = mock(IamClient.class);
CloudTasksUtils cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils();
// Individual permission.
User.revokeIapPermission("email@example.com", Optional.empty(), cloudTasksUtils, iamClient);
cloudTasksHelper.assertNoTasksEnqueued();
verify(iamClient).removeBinding("email@example.com", IAP_SECURED_WEB_APP_USER_ROLE);
// Group membership.
User.revokeIapPermission(
"email@example.com", Optional.of("console@example.com"), cloudTasksUtils, iamClient);
cloudTasksHelper.assertTasksEnqueued(
"console-user-group-update",
new TaskMatcher()
.service("TOOLS")
.method(HttpMethod.POST)
.path("/_dr/admin/updateUserGroup")
.param("userEmailAddress", "email@example.com")
.param("groupEmailAddress", "console@example.com")
.param("groupUpdateMode", "REMOVE"));
verifyNoMoreInteractions(iamClient);
}
}
@@ -54,6 +54,9 @@ import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationW
import google.registry.testing.FakeClock;
import google.registry.util.SerializeUtils;
import java.util.Arrays;
import java.util.Optional;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -69,7 +72,7 @@ public class DomainSqlTest {
new JpaTestExtensions.Builder().withClock(fakeClock).buildIntegrationWithCoverageExtension();
private Domain domain;
private VKey<Contact> contactKey;
private Optional<VKey<Contact>> contactKey;
private VKey<Contact> contact2Key;
private VKey<Host> host1VKey;
private Host host;
@@ -82,7 +85,7 @@ public class DomainSqlTest {
saveRegistrar("registrar1");
saveRegistrar("registrar2");
saveRegistrar("registrar3");
contactKey = createKey(Contact.class, "contact_id1");
contactKey = Optional.of(createKey(Contact.class, "contact_id1"));
contact2Key = createKey(Contact.class, "contact_id2");
host1VKey = createKey(Host.class, "host1");
@@ -133,10 +136,12 @@ public class DomainSqlTest {
new AllocationToken.Builder()
.setToken("abc123Unlimited")
.setTokenType(BULK_PRICING)
.setDiscountFraction(1.0)
.setCreationTimeForTest(DateTime.parse("2010-11-12T05:00:00Z"))
.setAllowedTlds(ImmutableSet.of("dev", "app"))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(CurrencyUnit.USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setTokenStatusTransitions(
ImmutableSortedMap.<DateTime, TokenStatus>naturalOrder()
@@ -185,7 +185,7 @@ public class DomainTest {
StatusValue.SERVER_UPDATE_PROHIBITED,
StatusValue.SERVER_RENEW_PROHIBITED,
StatusValue.SERVER_HOLD))
.setRegistrant(contact1Key)
.setRegistrant(Optional.of(contact1Key))
.setNameservers(ImmutableSet.of(hostKey))
.setSubordinateHosts(ImmutableSet.of("ns1.example.com"))
.setPersistedCurrentSponsorRegistrarId("NewRegistrar")
@@ -242,6 +242,16 @@ public class DomainTest {
.hasValue(domain);
}
@Test
void testRegistrantNotRequired() {
persistResource(domain.asBuilder().setRegistrant(Optional.empty()).build());
assertThat(
loadByForeignKey(Domain.class, domain.getForeignKey(), fakeClock.nowUtc())
.get()
.getRegistrant())
.isEmpty();
}
@Test
void testEmptyStringsBecomeNull() {
assertThat(
@@ -832,7 +842,9 @@ public class DomainTest {
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(BULK_PRICING)
.setDiscountFraction(1.0)
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.build());
@@ -894,7 +906,9 @@ public class DomainTest {
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(BULK_PRICING)
.setDiscountFraction(1.0)
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.build());
@@ -1012,17 +1026,17 @@ public class DomainTest {
DesignatedContact.create(Type.BILLING, contact3Key),
DesignatedContact.create(Type.TECH, contact4Key)),
true);
assertThat(domain.getRegistrant()).isEqualTo(contact1Key);
assertThat(domain.getAdminContact()).isEqualTo(contact2Key);
assertThat(domain.getBillingContact()).isEqualTo(contact3Key);
assertThat(domain.getTechContact()).isEqualTo(contact4Key);
assertThat(domain.getRegistrant()).hasValue(contact1Key);
assertThat(domain.getAdminContact()).hasValue(contact2Key);
assertThat(domain.getBillingContact()).hasValue(contact3Key);
assertThat(domain.getTechContact()).hasValue(contact4Key);
// Make sure everything gets nulled out.
domain.setContactFields(ImmutableSet.of(), true);
assertThat(domain.getRegistrant()).isNull();
assertThat(domain.getAdminContact()).isNull();
assertThat(domain.getBillingContact()).isNull();
assertThat(domain.getTechContact()).isNull();
assertThat(domain.getRegistrant()).isEmpty();
assertThat(domain.getAdminContact()).isEmpty();
assertThat(domain.getBillingContact()).isEmpty();
assertThat(domain.getTechContact()).isEmpty();
// Make sure that changes don't affect the registrant unless requested.
domain.setContactFields(
@@ -1032,16 +1046,16 @@ public class DomainTest {
DesignatedContact.create(Type.BILLING, contact3Key),
DesignatedContact.create(Type.TECH, contact4Key)),
false);
assertThat(domain.getRegistrant()).isNull();
assertThat(domain.getAdminContact()).isEqualTo(contact2Key);
assertThat(domain.getBillingContact()).isEqualTo(contact3Key);
assertThat(domain.getTechContact()).isEqualTo(contact4Key);
domain = domain.asBuilder().setRegistrant(contact1Key).build();
assertThat(domain.getRegistrant()).isEmpty();
assertThat(domain.getAdminContact()).hasValue(contact2Key);
assertThat(domain.getBillingContact()).hasValue(contact3Key);
assertThat(domain.getTechContact()).hasValue(contact4Key);
domain = domain.asBuilder().setRegistrant(Optional.of(contact1Key)).build();
domain.setContactFields(ImmutableSet.of(), false);
assertThat(domain.getRegistrant()).isEqualTo(contact1Key);
assertThat(domain.getAdminContact()).isNull();
assertThat(domain.getBillingContact()).isNull();
assertThat(domain.getTechContact()).isNull();
assertThat(domain.getRegistrant()).hasValue(contact1Key);
assertThat(domain.getAdminContact()).isEmpty();
assertThat(domain.getBillingContact()).isEmpty();
assertThat(domain.getTechContact()).isEmpty();
}
@Test
@@ -1064,7 +1078,9 @@ public class DomainTest {
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(BULK_PRICING)
.setDiscountFraction(1.0)
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.build();
@@ -1082,7 +1098,9 @@ public class DomainTest {
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(BULK_PRICING)
.setDiscountFraction(1.0)
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.build());
@@ -43,6 +43,8 @@ import google.registry.model.domain.token.AllocationToken.TokenStatus;
import google.registry.model.domain.token.AllocationToken.TokenType;
import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
import google.registry.util.SerializeUtils;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -158,15 +160,16 @@ public class AllocationTokenTest extends EntityTestCase {
@Test
void testSetRenewalBehavior_assertsRenewalBehaviorIsNotDefault() {
assertThat(
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.build())
.getRenewalPriceBehavior())
.isEqualTo(RenewalPriceBehavior.SPECIFIED);
AllocationToken token =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(CurrencyUnit.USD, 5))
.build());
assertThat(token.getRenewalPriceBehavior()).isEqualTo(RenewalPriceBehavior.SPECIFIED);
assertThat(token.getRenewalPrice()).hasValue(Money.of(CurrencyUnit.USD, 5));
}
@Test
@@ -181,7 +184,11 @@ public class AllocationTokenTest extends EntityTestCase {
AllocationToken loadedToken = loadByEntity(token);
assertThat(token).isEqualTo(loadedToken);
persistResource(
loadedToken.asBuilder().setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED).build());
loadedToken
.asBuilder()
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(CurrencyUnit.USD, 5))
.build());
assertThat(loadByEntity(token).getRenewalPriceBehavior())
.isEqualTo(RenewalPriceBehavior.SPECIFIED);
}
@@ -218,17 +225,50 @@ public class AllocationTokenTest extends EntityTestCase {
}
@Test
void testFail_bulkTokenNotSpecifiedRenewalBehavior() {
void testFail_bulkTokenInvalidRenewalBehavior() {
assertThat(
assertThrows(
IllegalArgumentException.class,
() ->
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(TokenType.BULK_PRICING)
.setDiscountFraction(1.0)
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setRenewalPriceBehavior(RenewalPriceBehavior.DEFAULT)
.build()))
.hasMessageThat()
.isEqualTo("BULK_PRICING tokens must have renewalPriceBehavior set to SPECIFIED");
assertThat(
assertThrows(
IllegalArgumentException.class,
() ->
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(TokenType.BULK_PRICING)
.setDiscountFraction(1.0)
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(CurrencyUnit.USD, 5))
.build()))
.hasMessageThat()
.isEqualTo("BULK_PRICING tokens must have a renewal price of 0");
}
@Test
void testFailure_bulkTokenDiscountFraction() {
AllocationToken.Builder builder =
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(TokenType.BULK_PRICING)
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setRenewalPriceBehavior(RenewalPriceBehavior.DEFAULT);
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(CurrencyUnit.USD, 0));
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, builder::build);
assertThat(thrown)
.hasMessageThat()
.isEqualTo("Bulk tokens must have renewalPriceBehavior set to SPECIFIED");
.isEqualTo("BULK_PRICING tokens must have a discountFraction of 1.0");
}
@Test
@@ -237,12 +277,14 @@ public class AllocationTokenTest extends EntityTestCase {
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(TokenType.BULK_PRICING)
.setDiscountFraction(1.0)
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE, CommandName.RESTORE))
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED);
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(CurrencyUnit.USD, 0));
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, builder::build);
assertThat(thrown)
.hasMessageThat()
.isEqualTo("Bulk tokens may only be valid for CREATE actions");
.isEqualTo("BULK_PRICING tokens may only be valid for CREATE actions");
}
@Test
@@ -263,11 +305,13 @@ public class AllocationTokenTest extends EntityTestCase {
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(TokenType.BULK_PRICING)
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED);
.setDiscountFraction(1.0)
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(CurrencyUnit.USD, 0));
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, builder::build);
assertThat(thrown)
.hasMessageThat()
.isEqualTo("Bulk tokens may only be valid for CREATE actions");
.isEqualTo("BULK_PRICING tokens may only be valid for CREATE actions");
}
@Test
@@ -276,11 +320,15 @@ public class AllocationTokenTest extends EntityTestCase {
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(TokenType.BULK_PRICING)
.setDiscountFraction(1.0)
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(CurrencyUnit.USD, 0))
.setDiscountPremiums(true);
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, builder::build);
assertThat(thrown).hasMessageThat().isEqualTo("Bulk tokens cannot discount premium names");
assertThat(thrown)
.hasMessageThat()
.isEqualTo("BULK_PRICING tokens cannot discount premium names");
}
@Test
@@ -338,8 +386,10 @@ public class AllocationTokenTest extends EntityTestCase {
new AllocationToken.Builder()
.setToken("foobar")
.setTokenType(BULK_PRICING)
.setDiscountFraction(1.0)
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(CurrencyUnit.USD, 0))
.setAllowedRegistrarIds(ImmutableSet.of("foo", "bar"));
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, builder::build);
assertThat(thrown)
@@ -536,6 +586,32 @@ public class AllocationTokenTest extends EntityTestCase {
.isEqualTo("Discount years can only be specified along with a discount fraction");
}
@Test
void testBuild_specifiedTokenInvalidBehavior() {
assertThat(
assertThrows(
IllegalArgumentException.class,
() ->
new AllocationToken.Builder()
.setToken("abc")
.setTokenType(SINGLE_USE)
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.build()))
.hasMessageThat()
.isEqualTo("renewalPrice must be specified iff renewalPriceBehavior is SPECIFIED");
assertThat(
assertThrows(
IllegalArgumentException.class,
() ->
new AllocationToken.Builder()
.setToken("abc")
.setTokenType(SINGLE_USE)
.setRenewalPrice(Money.of(CurrencyUnit.USD, 10))
.build()))
.hasMessageThat()
.isEqualTo("renewalPrice must be specified iff renewalPriceBehavior is SPECIFIED");
}
@Test
void testBuild_registrationBehaviors() {
createTld("tld");
@@ -564,6 +640,35 @@ public class AllocationTokenTest extends EntityTestCase {
.build();
}
@Test
void testFailures_badNonpremiumCreate() {
createTld("tld");
AllocationToken validToken =
new AllocationToken.Builder()
.setToken("abc")
.setTokenType(SINGLE_USE)
.setRegistrationBehavior(RegistrationBehavior.NONPREMIUM_CREATE)
.setDomainName("example.tld")
.build();
assertThrows(
IllegalArgumentException.class, () -> validToken.asBuilder().setDomainName(null).build());
assertThrows(
IllegalArgumentException.class,
() -> validToken.asBuilder().setDiscountFraction(0.5).build());
assertThrows(
IllegalArgumentException.class,
() ->
validToken
.asBuilder()
.setAllowedEppActions(
ImmutableSet.of(
CommandName.RENEW,
CommandName.TRANSFER,
CommandName.RESTORE,
CommandName.UPDATE))
.build());
}
private void assertBadInitialTransition(TokenStatus status) {
assertBadTransition(
ImmutableSortedMap.<DateTime, TokenStatus>naturalOrder()
@@ -55,8 +55,9 @@ public class BulkPricingPackageTest extends EntityTestCase {
.setAllowedTlds(ImmutableSet.of("foo"))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(CurrencyUnit.USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setDiscountFraction(1)
.setDiscountFraction(1.0)
.build());
BulkPricingPackage bulkPricingPackage =
@@ -84,7 +85,7 @@ public class BulkPricingPackageTest extends EntityTestCase {
.setCreationTimeForTest(DateTime.parse("2010-11-12T05:00:00Z"))
.setAllowedTlds(ImmutableSet.of("foo"))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setDiscountFraction(1)
.setDiscountFraction(1.0)
.build());
IllegalArgumentException thrown =
@@ -31,6 +31,7 @@ import google.registry.model.domain.Domain;
import google.registry.model.host.Host;
import google.registry.model.transfer.ContactTransferData;
import google.registry.persistence.VKey;
import java.util.Optional;
import org.joda.time.LocalDate;
import org.joda.time.format.ISODateTimeFormat;
import org.junit.jupiter.api.BeforeEach;
@@ -68,7 +69,7 @@ public final class Spec11ThreatMatchTest extends EntityTestCase {
.setDomainName("foo.tld")
.setRepoId(domainRepoId)
.setNameservers(hostVKey)
.setRegistrant(registrantContactVKey)
.setRegistrant(Optional.of(registrantContactVKey))
.setContacts(ImmutableSet.of())
.build();
@@ -15,6 +15,7 @@
package google.registry.rdap;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.DatabaseHelper.persistSimpleResources;
@@ -26,6 +27,7 @@ import static google.registry.testing.FullFieldsTestEntityHelper.makeRegistrarPo
import static google.registry.testing.GsonSubject.assertAboutJson;
import static org.mockito.Mockito.verify;
import com.google.common.collect.ImmutableSet;
import com.google.gson.JsonObject;
import google.registry.model.contact.Contact;
import google.registry.model.domain.Domain;
@@ -278,6 +280,33 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
assertProperResponseForCatLol("cat.lol", "rdap_domain_no_contacts_with_remark.json");
}
@Test
void testValidDomain_notLoggedIn_contactsShowRedacted_evenWhenRegistrantDoesntExist() {
// Even though the registrant is empty on this domain, it still shows a full set of REDACTED
// fields through RDAP.
persistResource(
loadByForeignKey(Domain.class, "cat.lol", clock.nowUtc())
.get()
.asBuilder()
.setRegistrant(Optional.empty())
.build());
assertProperResponseForCatLol("cat.lol", "rdap_domain_no_contacts_with_remark.json");
}
@Test
void testValidDomain_notLoggedIn_contactsShowRedacted_whenNoContactsExist() {
// Even though the domain has no contacts, it still shows a full set of REDACTED fields through
// RDAP.
persistResource(
loadByForeignKey(Domain.class, "cat.lol", clock.nowUtc())
.get()
.asBuilder()
.setRegistrant(Optional.empty())
.setContacts(ImmutableSet.of())
.build());
assertProperResponseForCatLol("cat.lol", "rdap_domain_no_contacts_exist_with_remark.json");
}
@Test
void testValidDomain_loggedInAsOtherRegistrar_noContacts() {
login("idnregistrar");
@@ -52,6 +52,8 @@ import google.registry.rdap.RdapObjectClasses.ReplyPayloadBase;
import google.registry.rdap.RdapObjectClasses.TopLevelReplyObject;
import google.registry.testing.FakeClock;
import google.registry.testing.FullFieldsTestEntityHelper;
import java.util.Optional;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -77,7 +79,7 @@ class RdapJsonFormatterTest {
private Host hostNoAddresses;
private Host hostNotLinked;
private Host hostSuperordinatePendingTransfer;
private Contact contactRegistrant;
@Nullable private Contact contactRegistrant;
private Contact contactAdmin;
private Contact contactTech;
private Contact contactNotLinked;
@@ -371,7 +373,7 @@ class RdapJsonFormatterTest {
.that(
rdapJsonFormatter
.createRdapContactEntity(
contactRegistrant,
Optional.of(contactRegistrant),
ImmutableSet.of(RdapEntity.Role.REGISTRANT),
OutputDataType.FULL)
.toJson())
@@ -384,7 +386,7 @@ class RdapJsonFormatterTest {
.that(
rdapJsonFormatter
.createRdapContactEntity(
contactRegistrant,
Optional.of(contactRegistrant),
ImmutableSet.of(RdapEntity.Role.REGISTRANT),
OutputDataType.SUMMARY)
.toJson())
@@ -398,7 +400,7 @@ class RdapJsonFormatterTest {
.that(
rdapJsonFormatter
.createRdapContactEntity(
contactRegistrant,
Optional.of(contactRegistrant),
ImmutableSet.of(RdapEntity.Role.REGISTRANT),
OutputDataType.FULL)
.toJson())
@@ -418,7 +420,7 @@ class RdapJsonFormatterTest {
.that(
rdapJsonFormatter
.createRdapContactEntity(
contactRegistrant,
Optional.of(contactRegistrant),
ImmutableSet.of(RdapEntity.Role.REGISTRANT),
OutputDataType.FULL)
.toJson())
@@ -431,7 +433,9 @@ class RdapJsonFormatterTest {
.that(
rdapJsonFormatter
.createRdapContactEntity(
contactAdmin, ImmutableSet.of(RdapEntity.Role.ADMIN), OutputDataType.FULL)
Optional.of(contactAdmin),
ImmutableSet.of(RdapEntity.Role.ADMIN),
OutputDataType.FULL)
.toJson())
.isEqualTo(loadJson("rdapjson_admincontact.json"));
}
@@ -442,7 +446,9 @@ class RdapJsonFormatterTest {
.that(
rdapJsonFormatter
.createRdapContactEntity(
contactTech, ImmutableSet.of(RdapEntity.Role.TECH), OutputDataType.FULL)
Optional.of(contactTech),
ImmutableSet.of(RdapEntity.Role.TECH),
OutputDataType.FULL)
.toJson())
.isEqualTo(loadJson("rdapjson_techcontact.json"));
}
@@ -452,7 +458,8 @@ class RdapJsonFormatterTest {
assertAboutJson()
.that(
rdapJsonFormatter
.createRdapContactEntity(contactTech, ImmutableSet.of(), OutputDataType.FULL)
.createRdapContactEntity(
Optional.of(contactTech), ImmutableSet.of(), OutputDataType.FULL)
.toJson())
.isEqualTo(loadJson("rdapjson_rolelesscontact.json"));
}
@@ -462,7 +469,8 @@ class RdapJsonFormatterTest {
assertAboutJson()
.that(
rdapJsonFormatter
.createRdapContactEntity(contactNotLinked, ImmutableSet.of(), OutputDataType.FULL)
.createRdapContactEntity(
Optional.of(contactNotLinked), ImmutableSet.of(), OutputDataType.FULL)
.toJson())
.isEqualTo(loadJson("rdapjson_unlinkedcontact.json"));
}
@@ -70,6 +70,7 @@ import google.registry.xjc.rdedomain.XjcRdeDomainElement;
import google.registry.xjc.rgp.XjcRgpStatusType;
import google.registry.xjc.secdns.XjcSecdnsDsDataType;
import java.io.ByteArrayOutputStream;
import java.util.Optional;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
@@ -280,9 +281,14 @@ public class DomainToXjcConverterTest {
makeHost(clock, "4-Q9JYB4C", "ns2.cat.みんな", "bad:f00d:cafe::15:beef")
.createVKey()))
.setRegistrant(
makeContact(
clock, "12-Q9JYB4C", "5372808-ERL", "(◕‿◕) nevermore", "prophet@evil.みんな")
.createVKey())
Optional.of(
makeContact(
clock,
"12-Q9JYB4C",
"5372808-ERL",
"(◕‿◕) nevermore",
"prophet@evil.みんな")
.createVKey()))
.setRegistrationExpirationTime(DateTime.parse("1930-01-01T00:00:00Z"))
.setGracePeriods(
ImmutableSet.of(
@@ -51,6 +51,7 @@ import google.registry.model.transfer.DomainTransferData;
import google.registry.model.transfer.TransferStatus;
import google.registry.testing.FakeClock;
import google.registry.util.Idn;
import java.util.Optional;
import org.joda.money.Money;
import org.joda.time.DateTime;
@@ -63,8 +64,9 @@ final class RdeFixtures {
.setDomainName("example." + tld)
.setRepoId(generateNewDomainRoid(tld))
.setRegistrant(
makeContact(clock, "5372808-ERL", "(◕‿◕) nevermore", "prophet@evil.みんな")
.createVKey())
Optional.of(
makeContact(clock, "5372808-ERL", "(◕‿◕) nevermore", "prophet@evil.みんな")
.createVKey()))
.build();
DomainHistory historyEntry =
persistResource(
@@ -135,7 +135,7 @@ public final class RegistryTestServerMain {
final RegistryTestServer server = new RegistryTestServer(address);
System.out.printf("%sLoading SQL fixtures and User service...%s\n", BLUE, RESET);
System.out.printf("%sLoading SQL fixtures setting User for authentication...%s\n", BLUE, RESET);
UserRoles userRoles =
new UserRoles.Builder().setIsAdmin(loginIsAdmin).setGlobalRole(GlobalRole.FTE).build();
User user =
@@ -151,7 +151,7 @@ public final class RegistryTestServerMain {
for (Fixture fixture : fixtures) {
fixture.load();
}
System.out.printf("%sStarting Jetty6 HTTP Server...%s\n", BLUE, RESET);
System.out.printf("%sStarting Jetty HTTP Server...%s\n", BLUE, RESET);
server.start();
System.out.printf("%sListening on: %s%s\n", PURPLE, server.getUrl("/"), RESET);
try {
@@ -24,7 +24,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.net.HostAndPort;
import com.google.common.util.concurrent.SimpleTimeLimiter;
import google.registry.ui.server.registrar.RegistrarSettingsAction;
import google.registry.util.RegistryEnvironment;
import google.registry.util.UrlChecker;
import jakarta.servlet.http.HttpServlet;
import java.net.MalformedURLException;
@@ -91,7 +91,7 @@ public final class TestServer {
/** Starts the HTTP server in a new thread and returns once it's online. */
public void start() {
try {
RegistrarSettingsAction.setIsInTestDriverToTrue();
RegistryEnvironment.setIsInTestDriver(true);
server.start();
} catch (Exception e) {
throwIfUnchecked(e);
@@ -129,7 +129,7 @@ public final class TestServer {
.callWithTimeout(
() -> {
server.stop();
RegistrarSettingsAction.setIsInTestDriverToFalse();
RegistryEnvironment.setIsInTestDriver(false);
return null;
},
SHUTDOWN_TIMEOUT_MS,
@@ -80,12 +80,12 @@ import org.joda.time.DateTime;
* to the same test task container that the original instance pushes to, so that we can make
* assertions on them by accessing the original instance. We cannot make the test task container
* itself static because we do not want tasks enqueued in previous tests to interfere with latter
* tests, when they run on the same JVM (and therefore share the same static class members). To
* solve this we put the test container in a static map whose keys are the instance IDs. An
* explicitly created new {@link CloudTasksHelper} (as would be created for a new test method) would
* have a new ID allocated to it, and therefore stores its tasks in a distinct container. A
* deserialized {@link CloudTasksHelper}, on the other hand, will have the same instance ID and
* share the same test class container with its progenitor.
* tests when they run on the same JVM (and therefore share the same static class members). To solve
* this, we put the test container in a static map whose keys are the instance IDs. An explicitly
* created new {@link CloudTasksHelper} (as would be created for a new test method) would have a new
* ID allocated to it, and therefore stores its tasks in a distinct container. A deserialized {@link
* CloudTasksHelper}, on the other hand, will have the same instance ID and share the same test
* class container with its progenitor.
*/
public class CloudTasksHelper implements Serializable {
@@ -131,7 +131,7 @@ public class CloudTasksHelper implements Serializable {
*/
public void assertTasksEnqueuedWithProperty(
String queueName, Function<Task, String> propertyGetter, String... expectedTaskProperties) {
// Ordering is irrelevant but duplicates should be considered independently.
// Ordering is irrelevant, but duplicates should be considered independently.
assertThat(getTestTasksFor(queueName).stream().map(propertyGetter))
.containsExactly((Object[]) expectedTaskProperties);
}
@@ -285,7 +285,7 @@ public class CloudTasksHelper implements Serializable {
});
headers = headerBuilder.build();
ImmutableMultimap.Builder<String, String> paramBuilder = new ImmutableMultimap.Builder<>();
// Note that UriParameters.parse() does not throw an IAE on a bad query string (e.g. one
// Note that UriParameters.parse() does not throw an IAE on a bad query string (e.g., one
// where parameters are not properly URL-encoded); it always does a best-effort parse.
if (method == HttpMethod.GET && uri.getQuery() != null) {
paramBuilder.putAll(UriParameters.parse(uri.getQuery()));
@@ -382,7 +382,7 @@ public class CloudTasksHelper implements Serializable {
* the same contract as {@link #equals}, since it will ignore null fields.
*
* <p>Match fails if any headers or params expected on the TaskMatcher are not found on the
* Task. Note that the inverse is not true (i.e. there may be extra headers on the Task).
* Task. Note that the inverse is not true (i.e., there may be extra headers on the Task).
*
* <p>Schedule time by default is Timestamp.getDefaultInstance() or null.
*/
@@ -190,7 +190,7 @@ public final class DatabaseHelper {
.setPersistedCurrentSponsorRegistrarId("TheRegistrar")
.setCreationTimeForTest(START_OF_TIME)
.setAuthInfo(DomainAuthInfo.create(PasswordAuth.create("2fooBAR")))
.setRegistrant(contactKey)
.setRegistrant(Optional.of(contactKey))
.setContacts(
ImmutableSet.of(
DesignatedContact.create(Type.ADMIN, contactKey),
@@ -603,7 +603,7 @@ public final class DatabaseHelper {
.setCreationRegistrarId("TheRegistrar")
.setCreationTimeForTest(creationTime)
.setRegistrationExpirationTime(expirationTime)
.setRegistrant(contact.createVKey())
.setRegistrant(Optional.of(contact.createVKey()))
.setContacts(
ImmutableSet.of(
DesignatedContact.create(Type.ADMIN, contact.createVKey()),
@@ -23,6 +23,7 @@ import com.google.common.base.Throwables;
import com.google.common.net.MediaType;
import google.registry.request.Response;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
@@ -69,7 +70,7 @@ public final class FakeResponse implements Response {
@Override
public void sendRedirect(String url) throws IOException {
status = 302;
status = HttpServletResponse.SC_FOUND;
this.payload = String.format("Redirected to %s", url);
}
@@ -46,6 +46,7 @@ import google.registry.persistence.VKey;
import google.registry.util.Idn;
import java.net.InetAddress;
import java.util.List;
import java.util.Optional;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
@@ -351,7 +352,7 @@ public final class FullFieldsTestEntityHelper {
StatusValue.SERVER_UPDATE_PROHIBITED))
.setDsData(ImmutableSet.of(DomainDsData.create(1, 2, 3, "deadface")));
if (registrant != null) {
builder.setRegistrant(registrant.createVKey());
builder.setRegistrant(Optional.of(registrant.createVKey()));
}
if ((admin != null) || (tech != null)) {
ImmutableSet.Builder<DesignatedContact> contactsBuilder = new ImmutableSet.Builder<>();
@@ -0,0 +1,208 @@
// 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.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.FeatureName.TEST_FEATURE;
import static google.registry.model.common.FeatureFlag.FeatureStatus.ACTIVE;
import static google.registry.model.common.FeatureFlag.FeatureStatus.INACTIVE;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.beust.jcommander.ParameterException;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.model.common.FeatureFlag;
import google.registry.model.common.FeatureFlag.FeatureStatus;
import google.registry.model.common.TimedTransitionProperty;
import google.registry.testing.FakeClock;
import org.joda.time.DateTime;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link ConfigureFeatureFlagCommand}. */
public class ConfigureFeatureFlagCommandTest extends CommandTestCase<ConfigureFeatureFlagCommand> {
private final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01T00:00:00Z"));
@Test
void testCreate() throws Exception {
DateTime featureStart = clock.nowUtc().plusWeeks(2);
runCommandForced(
"TEST_FEATURE",
"--status_map",
String.format("%s=INACTIVE,%s=ACTIVE", START_OF_TIME, featureStart));
assertThat(FeatureFlag.get(TEST_FEATURE).getStatusMap())
.isEqualTo(
TimedTransitionProperty.fromValueMap(
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, featureStart, ACTIVE)));
assertThat(FeatureFlag.getAllUncached()).hasSize(1);
}
@Test
void testCreate_multipleFlags() throws Exception {
DateTime featureStart = clock.nowUtc().plusWeeks(2);
runCommandForced(
"TEST_FEATURE",
"MINIMUM_DATASET_CONTACTS_OPTIONAL",
"MINIMUM_DATASET_CONTACTS_PROHIBITED",
"--status_map",
String.format("%s=INACTIVE,%s=ACTIVE", START_OF_TIME, featureStart));
assertThat(FeatureFlag.get(TEST_FEATURE).getStatusMap())
.isEqualTo(
TimedTransitionProperty.fromValueMap(
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, featureStart, ACTIVE)));
assertThat(FeatureFlag.get(MINIMUM_DATASET_CONTACTS_OPTIONAL).getStatusMap())
.isEqualTo(
TimedTransitionProperty.fromValueMap(
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, featureStart, ACTIVE)));
assertThat(FeatureFlag.get(MINIMUM_DATASET_CONTACTS_PROHIBITED).getStatusMap())
.isEqualTo(
TimedTransitionProperty.fromValueMap(
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, featureStart, ACTIVE)));
assertThat(FeatureFlag.getAllUncached()).hasSize(3);
}
@Test
void testUpdate() throws Exception {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(TEST_FEATURE)
.setStatusMap(
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
.put(START_OF_TIME, INACTIVE)
.build())
.build());
DateTime featureStart = clock.nowUtc().plusWeeks(6);
assertThat(FeatureFlag.get(TEST_FEATURE).getStatusMap())
.isEqualTo(
TimedTransitionProperty.fromValueMap(ImmutableSortedMap.of(START_OF_TIME, INACTIVE)));
assertThat(FeatureFlag.getAllUncached()).hasSize(1);
runCommandForced(
"TEST_FEATURE",
"--status_map",
String.format("%s=INACTIVE,%s=ACTIVE", START_OF_TIME, featureStart));
assertThat(FeatureFlag.get(TEST_FEATURE).getStatusMap())
.isEqualTo(
TimedTransitionProperty.fromValueMap(
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, featureStart, ACTIVE)));
assertThat(FeatureFlag.getAllUncached()).hasSize(1);
}
@Test
void testConfigure_multipleFlags() throws Exception {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(TEST_FEATURE)
.setStatusMap(
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
.put(START_OF_TIME, INACTIVE)
.build())
.build());
DateTime featureStart = clock.nowUtc().plusWeeks(6);
assertThat(FeatureFlag.get(TEST_FEATURE).getStatusMap())
.isEqualTo(
TimedTransitionProperty.fromValueMap(ImmutableSortedMap.of(START_OF_TIME, INACTIVE)));
assertThat(FeatureFlag.getAllUncached()).hasSize(1);
runCommandForced(
"TEST_FEATURE",
"MINIMUM_DATASET_CONTACTS_OPTIONAL",
"MINIMUM_DATASET_CONTACTS_PROHIBITED",
"--status_map",
String.format("%s=INACTIVE,%s=ACTIVE", START_OF_TIME, featureStart));
assertThat(FeatureFlag.get(TEST_FEATURE).getStatusMap())
.isEqualTo(
TimedTransitionProperty.fromValueMap(
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, featureStart, ACTIVE)));
assertThat(FeatureFlag.get(MINIMUM_DATASET_CONTACTS_OPTIONAL).getStatusMap())
.isEqualTo(
TimedTransitionProperty.fromValueMap(
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, featureStart, ACTIVE)));
assertThat(FeatureFlag.get(MINIMUM_DATASET_CONTACTS_PROHIBITED).getStatusMap())
.isEqualTo(
TimedTransitionProperty.fromValueMap(
ImmutableSortedMap.of(START_OF_TIME, INACTIVE, featureStart, ACTIVE)));
assertThat(FeatureFlag.getAllUncached()).hasSize(3);
}
@Test
void testCreate_invalidFeatureName() throws Exception {
ParameterException thrown =
assertThrows(
ParameterException.class,
() ->
runCommandForced(
"INVALID_NAME", "--status_map", String.format("%s=ACTIVE", START_OF_TIME)));
assertThat(thrown)
.hasMessageThat()
.contains("Invalid value for [Main class] parameter. Allowed values");
}
@Test
void testCreate_invalidStatusMap() throws Exception {
DateTime featureStart = clock.nowUtc().plusWeeks(2);
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
runCommandForced(
"TEST_FEATURE", "--status_map", String.format("%s=ACTIVE", featureStart)));
assertThat(thrown)
.hasMessageThat()
.isEqualTo("Must provide transition entry for the start of time (Unix Epoch)");
}
@Test
void testUpdate_invalidStatusMap() throws Exception {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(TEST_FEATURE)
.setStatusMap(
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
.put(START_OF_TIME, INACTIVE)
.build())
.build());
DateTime featureStart = clock.nowUtc().plusWeeks(6);
assertThat(FeatureFlag.get(TEST_FEATURE).getStatusMap())
.isEqualTo(
TimedTransitionProperty.fromValueMap(ImmutableSortedMap.of(START_OF_TIME, INACTIVE)));
assertThat(FeatureFlag.getAllUncached()).hasSize(1);
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
runCommandForced(
"TEST_FEATURE", "--status_map", String.format("%s=ACTIVE", featureStart)));
assertThat(thrown)
.hasMessageThat()
.isEqualTo("Must provide transition entry for the start of time (Unix Epoch)");
}
}
@@ -18,6 +18,7 @@ import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static org.joda.money.CurrencyUnit.USD;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableSet;
@@ -46,8 +47,9 @@ public class CreateBulkPricingPackageCommandTest
.setAllowedTlds(ImmutableSet.of("foo"))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setDiscountFraction(1)
.setDiscountFraction(1.0)
.build());
runCommandForced(
"--max_domains=100",
@@ -78,7 +80,8 @@ public class CreateBulkPricingPackageCommandTest
.setAllowedTlds(ImmutableSet.of("foo"))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setDiscountFraction(1)
.setRenewalPrice(Money.of(USD, 0))
.setDiscountFraction(1.0)
.build());
IllegalArgumentException thrown =
assertThrows(
@@ -122,8 +125,9 @@ public class CreateBulkPricingPackageCommandTest
.setAllowedTlds(ImmutableSet.of("foo"))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setDiscountFraction(1)
.setDiscountFraction(1.0)
.build());
runCommandForced(
"--max_domains=100",
@@ -158,8 +162,9 @@ public class CreateBulkPricingPackageCommandTest
.setAllowedTlds(ImmutableSet.of("foo"))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setDiscountFraction(1)
.setDiscountFraction(1.0)
.build());
runCommandForced("--price=USD 1000.00", "--next_billing_date=2012-03-17T00:00:00Z", "abc123");
Optional<BulkPricingPackage> bulkPricingPackageOptional =
@@ -184,8 +189,9 @@ public class CreateBulkPricingPackageCommandTest
.setAllowedTlds(ImmutableSet.of("foo"))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setDiscountFraction(1)
.setDiscountFraction(1.0)
.build());
runCommandForced("--max_domains=100", "--max_creates=500", "--price=USD 1000.00", "abc123");
@@ -210,8 +216,9 @@ public class CreateBulkPricingPackageCommandTest
.setAllowedTlds(ImmutableSet.of("foo"))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setDiscountFraction(1)
.setDiscountFraction(1.0)
.build());
IllegalArgumentException thrown =
assertThrows(
@@ -15,20 +15,22 @@
package google.registry.tools;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.tools.CreateUserCommand.IAP_SECURED_WEB_APP_USER_ROLE;
import static google.registry.model.console.User.IAP_SECURED_WEB_APP_USER_ROLE;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import com.google.cloud.tasks.v2.HttpMethod;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.net.MediaType;
import google.registry.model.console.GlobalRole;
import google.registry.model.console.RegistrarRole;
import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import google.registry.testing.CloudTasksHelper;
import google.registry.testing.CloudTasksHelper.TaskMatcher;
import google.registry.testing.DatabaseHelper;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
@@ -38,13 +40,13 @@ import org.junit.jupiter.api.Test;
public class CreateUserCommandTest extends CommandTestCase<CreateUserCommand> {
private final IamClient iamClient = mock(IamClient.class);
private final ServiceConnection connection = mock(ServiceConnection.class);
private final CloudTasksHelper cloudTasksHelper = new CloudTasksHelper();
@BeforeEach
void beforeEach() {
command.iamClient = iamClient;
command.maybeGroupEmailAddress = Optional.empty();
command.setConnection(connection);
command.cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils();
}
@Test
@@ -57,7 +59,7 @@ public class CreateUserCommandTest extends CommandTestCase<CreateUserCommand> {
assertThat(onlyUser.getUserRoles().getRegistrarRoles()).isEmpty();
verify(iamClient).addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE);
verifyNoMoreInteractions(iamClient);
verifyNoInteractions(connection);
cloudTasksHelper.assertNoTasksEnqueued("console-user-group-update");
}
@Test
@@ -69,20 +71,16 @@ public class CreateUserCommandTest extends CommandTestCase<CreateUserCommand> {
assertThat(onlyUser.getUserRoles().isAdmin()).isFalse();
assertThat(onlyUser.getUserRoles().getGlobalRole()).isEqualTo(GlobalRole.NONE);
assertThat(onlyUser.getUserRoles().getRegistrarRoles()).isEmpty();
verify(connection)
.sendPostRequest(
"/_dr/admin/updateUserGroup",
ImmutableMap.of(
"userEmailAddress",
"user@example.test",
"groupEmailAddress",
"group@example.test",
"groupUpdateMode",
"ADD"),
MediaType.PLAIN_TEXT_UTF_8,
new byte[0]);
cloudTasksHelper.assertTasksEnqueued(
"console-user-group-update",
new TaskMatcher()
.method(HttpMethod.POST)
.service("TOOLS")
.path("/_dr/admin/updateUserGroup")
.param("userEmailAddress", "user@example.test")
.param("groupEmailAddress", "group@example.test")
.param("groupUpdateMode", "ADD"));
verifyNoInteractions(iamClient);
verifyNoMoreInteractions(connection);
}
@Test
@@ -102,7 +100,7 @@ public class CreateUserCommandTest extends CommandTestCase<CreateUserCommand> {
assertThat(UserDao.loadUser("user@example.test").get().getUserRoles().isAdmin()).isTrue();
verify(iamClient).addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE);
verifyNoMoreInteractions(iamClient);
verifyNoInteractions(connection);
cloudTasksHelper.assertNoTasksEnqueued("console-user-group-update");
}
@Test
@@ -112,7 +110,7 @@ public class CreateUserCommandTest extends CommandTestCase<CreateUserCommand> {
.isEqualTo(GlobalRole.FTE);
verify(iamClient).addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE);
verifyNoMoreInteractions(iamClient);
verifyNoInteractions(connection);
cloudTasksHelper.assertNoTasksEnqueued("console-user-group-update");
}
@Test
@@ -131,7 +129,7 @@ public class CreateUserCommandTest extends CommandTestCase<CreateUserCommand> {
RegistrarRole.PRIMARY_CONTACT));
verify(iamClient).addBinding("user@example.test", IAP_SECURED_WEB_APP_USER_ROLE);
verifyNoMoreInteractions(iamClient);
verifyNoInteractions(connection);
cloudTasksHelper.assertNoTasksEnqueued("console-user-group-update");
}
@Test
@@ -146,7 +144,7 @@ public class CreateUserCommandTest extends CommandTestCase<CreateUserCommand> {
.hasMessageThat()
.isEqualTo("A user with email user@example.test already exists");
verifyNoMoreInteractions(iamClient);
verifyNoInteractions(connection);
cloudTasksHelper.assertNoTasksEnqueued("console-user-group-update");
}
@Test
@@ -15,16 +15,17 @@
package google.registry.tools;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.tools.CreateUserCommand.IAP_SECURED_WEB_APP_USER_ROLE;
import static google.registry.model.console.User.IAP_SECURED_WEB_APP_USER_ROLE;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import com.google.common.collect.ImmutableMap;
import com.google.common.net.MediaType;
import com.google.cloud.tasks.v2.HttpMethod;
import google.registry.model.console.UserDao;
import google.registry.testing.CloudTasksHelper;
import google.registry.testing.CloudTasksHelper.TaskMatcher;
import google.registry.testing.DatabaseHelper;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
@@ -34,13 +35,13 @@ import org.junit.jupiter.api.Test;
public class DeleteUserCommandTest extends CommandTestCase<DeleteUserCommand> {
private final IamClient iamClient = mock(IamClient.class);
private final ServiceConnection connection = mock(ServiceConnection.class);
private final CloudTasksHelper cloudTasksHelper = new CloudTasksHelper();
@BeforeEach
void beforeEach() {
command.iamClient = iamClient;
command.setConnection(connection);
command.maybeGroupEmailAddress = Optional.empty();
command.cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils();
}
@Test
@@ -51,7 +52,7 @@ public class DeleteUserCommandTest extends CommandTestCase<DeleteUserCommand> {
assertThat(UserDao.loadUser("email@example.test")).isEmpty();
verify(iamClient).removeBinding("email@example.test", IAP_SECURED_WEB_APP_USER_ROLE);
verifyNoMoreInteractions(iamClient);
verifyNoInteractions(connection);
cloudTasksHelper.assertNoTasksEnqueued("console-user-group-update");
}
@Test
@@ -61,20 +62,16 @@ public class DeleteUserCommandTest extends CommandTestCase<DeleteUserCommand> {
assertThat(UserDao.loadUser("email@example.test")).isPresent();
runCommandForced("--email", "email@example.test");
assertThat(UserDao.loadUser("email@example.test")).isEmpty();
verify(connection)
.sendPostRequest(
"/_dr/admin/updateUserGroup",
ImmutableMap.of(
"userEmailAddress",
"email@example.test",
"groupEmailAddress",
"group@example.test",
"groupUpdateMode",
"REMOVE"),
MediaType.PLAIN_TEXT_UTF_8,
new byte[0]);
cloudTasksHelper.assertTasksEnqueued(
"console-user-group-update",
new TaskMatcher()
.method(HttpMethod.POST)
.service("TOOLS")
.path("/_dr/admin/updateUserGroup")
.param("userEmailAddress", "email@example.test")
.param("groupEmailAddress", "group@example.test")
.param("groupUpdateMode", "REMOVE"));
verifyNoInteractions(iamClient);
verifyNoMoreInteractions(connection);
}
@Test
@@ -86,6 +83,6 @@ public class DeleteUserCommandTest extends CommandTestCase<DeleteUserCommand> {
.hasMessageThat()
.isEqualTo("Email does not correspond to a valid user");
verifyNoInteractions(iamClient);
verifyNoInteractions(connection);
cloudTasksHelper.assertNoTasksEnqueued("console-user-group-update");
}
}
@@ -45,6 +45,8 @@ import google.registry.util.StringGenerator.Alphabets;
import java.io.File;
import java.util.Collection;
import javax.annotation.Nullable;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -194,19 +196,47 @@ class GenerateAllocationTokensCommandTest extends CommandTestCase<GenerateAlloca
@Test
void testSuccess_renewalPriceBehaviorIsSpecified() throws Exception {
runCommand("--tokens", "foobar,foobaz", "--renewal_price_behavior", "SPECIFIED");
runCommand(
"--tokens",
"foobar,foobaz",
"--renewal_price_behavior",
"SPECIFIED",
"--renewal_price",
"USD 10");
assertAllocationTokens(
createToken("foobar", null, null).asBuilder().setRenewalPriceBehavior(SPECIFIED).build(),
createToken("foobaz", null, null).asBuilder().setRenewalPriceBehavior(SPECIFIED).build());
createToken("foobar", null, null)
.asBuilder()
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(CurrencyUnit.USD, 10))
.build(),
createToken("foobaz", null, null)
.asBuilder()
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(CurrencyUnit.USD, 10))
.build());
assertInStdout("foobar", "foobaz");
}
@Test
void testSuccess_renewalPriceBehaviorIsSpecifiedButMixedCase() throws Exception {
runCommand("--tokens", "foobar,foobaz", "--renewal_price_behavior", "speCIFied");
runCommand(
"--tokens",
"foobar,foobaz",
"--renewal_price_behavior",
"speCIFied",
"--renewal_price",
"USD 10");
assertAllocationTokens(
createToken("foobar", null, null).asBuilder().setRenewalPriceBehavior(SPECIFIED).build(),
createToken("foobaz", null, null).asBuilder().setRenewalPriceBehavior(SPECIFIED).build());
createToken("foobar", null, null)
.asBuilder()
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(CurrencyUnit.USD, 10))
.build(),
createToken("foobaz", null, null)
.asBuilder()
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(CurrencyUnit.USD, 10))
.build());
assertInStdout("foobar", "foobaz");
}
@@ -236,6 +266,16 @@ class GenerateAllocationTokensCommandTest extends CommandTestCase<GenerateAlloca
+ " NONPREMIUM, SPECIFIED]");
}
@Test
void testFailure_specifiedPrice_withoutPrice() throws Exception {
assertThat(
assertThrows(
IllegalArgumentException.class,
() -> runCommand("--tokens", "foobar", "--renewal_price_behavior", "SPECIFIED")))
.hasMessageThat()
.isEqualTo("renewal_price must be specified iff renewal_price_behavior is SPECIFIED");
}
@Test
void testSuccess_defaultRegistrationBehavior() throws Exception {
runCommand("--tokens", "foobar,blah");
@@ -17,6 +17,7 @@ package google.registry.tools;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.persistResource;
import static org.joda.money.CurrencyUnit.USD;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.beust.jcommander.ParameterException;
@@ -52,8 +53,9 @@ public class GetBulkPricingPackageCommandTest
.setAllowedTlds(ImmutableSet.of("foo"))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setDiscountFraction(1)
.setDiscountFraction(1.0)
.build());
BulkPricingPackage bulkPricingPackage =
new BulkPricingPackage.Builder()
@@ -79,8 +81,9 @@ public class GetBulkPricingPackageCommandTest
.setAllowedTlds(ImmutableSet.of("foo"))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setDiscountFraction(1)
.setDiscountFraction(1.0)
.build());
tm().transact(
() ->
@@ -102,8 +105,9 @@ public class GetBulkPricingPackageCommandTest
.setAllowedTlds(ImmutableSet.of("foo"))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setDiscountFraction(1)
.setDiscountFraction(1.0)
.build());
tm().transact(
() ->
@@ -0,0 +1,130 @@
// 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.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.TEST_FEATURE;
import static google.registry.model.common.FeatureFlag.FeatureStatus.ACTIVE;
import static google.registry.model.common.FeatureFlag.FeatureStatus.INACTIVE;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.beust.jcommander.ParameterException;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.model.EntityYamlUtils;
import google.registry.model.common.FeatureFlag;
import google.registry.model.common.FeatureFlag.FeatureFlagNotFoundException;
import google.registry.model.common.FeatureFlag.FeatureStatus;
import google.registry.testing.FakeClock;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link GetFeatureFlagCommand}. */
public class GetFeatureFlagCommandTest extends CommandTestCase<GetFeatureFlagCommand> {
private final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01T00:00:00Z"));
@BeforeEach
void beforeEach() {
command.objectMapper = EntityYamlUtils.createObjectMapper();
}
@Test
void testSuccess() throws Exception {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(TEST_FEATURE)
.setStatusMap(
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
.put(START_OF_TIME, INACTIVE)
.put(clock.nowUtc().plusWeeks(8), ACTIVE)
.build())
.build());
runCommand("TEST_FEATURE");
assertInStdout(
"featureName: \"TEST_FEATURE\"\n"
+ "status:\n"
+ " \"1970-01-01T00:00:00.000Z\": \"INACTIVE\"\n"
+ " \"2000-02-26T00:00:00.000Z\": \"ACTIVE\"");
}
@Test
void testSuccess_multipleArguments() throws Exception {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(TEST_FEATURE)
.setStatusMap(
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
.put(START_OF_TIME, INACTIVE)
.put(clock.nowUtc().plusWeeks(8), ACTIVE)
.build())
.build());
persistResource(
new FeatureFlag.Builder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
.setStatusMap(
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
.put(START_OF_TIME, INACTIVE)
.put(clock.nowUtc().plusWeeks(3), ACTIVE)
.put(clock.nowUtc().plusWeeks(6), INACTIVE)
.build())
.build());
runCommand("TEST_FEATURE", "MINIMUM_DATASET_CONTACTS_OPTIONAL");
assertInStdout(
"featureName: \"TEST_FEATURE\"\n"
+ "status:\n"
+ " \"1970-01-01T00:00:00.000Z\": \"INACTIVE\"\n"
+ " \"2000-02-26T00:00:00.000Z\": \"ACTIVE\""
+ "\n\n"
+ "featureName: \"MINIMUM_DATASET_CONTACTS_OPTIONAL\"\n"
+ "status:\n"
+ " \"1970-01-01T00:00:00.000Z\": \"INACTIVE\"\n"
+ " \"2000-01-22T00:00:00.000Z\": \"ACTIVE\"\n"
+ " \"2000-02-12T00:00:00.000Z\": \"INACTIVE\"");
}
@Test
void testFailure_featureFlagDoesNotExist() {
FeatureFlagNotFoundException thrown =
assertThrows(FeatureFlagNotFoundException.class, () -> runCommand("TEST_FEATURE"));
assertThat(thrown)
.hasMessageThat()
.isEqualTo("No feature flag object(s) found for TEST_FEATURE");
}
@Test
void testFailure_oneFlagDoesNotExist() {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(TEST_FEATURE)
.setStatusMap(
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
.put(START_OF_TIME, INACTIVE)
.put(clock.nowUtc().plusWeeks(8), ACTIVE)
.build())
.build());
assertThrows(
FeatureFlagNotFoundException.class,
() -> runCommand("TEST_FEATURE", "MINIMUM_DATASET_CONTACTS_OPTIONAL"));
}
@Test
void testFailure_noTldName() {
assertThrows(ParameterException.class, this::runCommand);
}
}
@@ -0,0 +1,89 @@
// 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.tools;
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.FeatureName.TEST_FEATURE;
import static google.registry.model.common.FeatureFlag.FeatureStatus.ACTIVE;
import static google.registry.model.common.FeatureFlag.FeatureStatus.INACTIVE;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.TestDataHelper.loadFile;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.model.EntityYamlUtils;
import google.registry.model.common.FeatureFlag;
import google.registry.model.common.FeatureFlag.FeatureStatus;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class ListFeatureFlagsCommandTest extends CommandTestCase<ListFeatureFlagsCommand> {
@BeforeEach
void beforeEach() {
command.mapper = EntityYamlUtils.createObjectMapper();
fakeClock.setTo(DateTime.parse("1984-12-21T06:07:08.789Z"));
}
@Test
void testSuccess_oneFlag() throws Exception {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(TEST_FEATURE)
.setStatusMap(
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
.put(START_OF_TIME, INACTIVE)
.put(fakeClock.nowUtc().plusWeeks(8), ACTIVE)
.build())
.build());
runCommand();
assertInStdout(loadFile(getClass(), "oneFlag.yaml"));
}
@Test
void test_success_manyFlags() throws Exception {
persistResource(
new FeatureFlag.Builder()
.setFeatureName(TEST_FEATURE)
.setStatusMap(
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
.put(START_OF_TIME, INACTIVE)
.put(fakeClock.nowUtc().plusWeeks(8), ACTIVE)
.build())
.build());
persistResource(
new FeatureFlag.Builder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
.setStatusMap(
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
.put(START_OF_TIME, INACTIVE)
.put(fakeClock.nowUtc().plusWeeks(1), ACTIVE)
.put(fakeClock.nowUtc().plusWeeks(8), INACTIVE)
.put(fakeClock.nowUtc().plusWeeks(10), ACTIVE)
.build())
.build());
persistResource(
new FeatureFlag.Builder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_PROHIBITED)
.setStatusMap(
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
.put(START_OF_TIME, ACTIVE)
.build())
.build());
runCommand();
assertInStdout(loadFile(getClass(), "threeFlags.yaml"));
}
}
@@ -15,6 +15,8 @@
package google.registry.tools;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.OteAccountBuilderTest.verifyUser;
import static google.registry.model.console.User.IAP_SECURED_WEB_APP_USER_ROLE;
import static google.registry.model.registrar.RegistrarBase.State.ACTIVE;
import static google.registry.model.tld.Tld.TldState.GENERAL_AVAILABILITY;
import static google.registry.model.tld.Tld.TldState.START_DATE_SUNRISE;
@@ -26,19 +28,30 @@ import static google.registry.testing.DatabaseHelper.persistResource;
import static org.joda.money.CurrencyUnit.USD;
import static org.joda.time.DateTimeZone.UTC;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import com.beust.jcommander.ParameterException;
import com.google.cloud.tasks.v2.HttpMethod;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.model.console.GlobalRole;
import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import google.registry.model.console.UserRoles;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.model.tld.Tld;
import google.registry.model.tld.Tld.TldState;
import google.registry.testing.CloudTasksHelper;
import google.registry.testing.CloudTasksHelper.TaskMatcher;
import google.registry.testing.DeterministicStringGenerator;
import google.registry.testing.FakeClock;
import google.registry.util.CidrAddressBlock;
import java.security.cert.CertificateParsingException;
import java.util.Optional;
import javax.annotation.Nullable;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.joda.time.Duration;
@@ -50,22 +63,24 @@ class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
private static final String PASSWORD = "abcdefghijklmnop";
private DeterministicStringGenerator passwordGenerator =
private final IamClient iamClient = mock(IamClient.class);
private final CloudTasksHelper cloudTasksHelper = new CloudTasksHelper();
private final DeterministicStringGenerator passwordGenerator =
new DeterministicStringGenerator("abcdefghijklmnopqrstuvwxyz");
@BeforeEach
void beforeEach() {
command.passwordGenerator = passwordGenerator;
command.clock = new FakeClock(DateTime.parse("2018-07-07TZ"));
command.maybeGroupEmailAddress = Optional.of("group@example.com");
command.cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils();
command.iamClient = iamClient;
persistPremiumList("default_sandbox_list", USD, "sandbox,USD 1000");
}
/** Verify TLD creation. */
private void verifyTldCreation(
String tldName,
String roidSuffix,
TldState tldState,
boolean isEarlyAccess) {
String tldName, String roidSuffix, TldState tldState, boolean isEarlyAccess) {
Tld registry = Tld.get(tldName);
assertThat(registry).isNotNull();
assertThat(registry.getRoidSuffix()).isEqualTo(roidSuffix);
@@ -107,13 +122,28 @@ class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
assertThat(registrar.getClientCertificateHash()).hasValue(SAMPLE_CERT_HASH);
}
private void verifyRegistrarContactCreation(String registrarName, String email) {
ImmutableSet<RegistrarPoc> registrarPocs = loadRegistrar(registrarName).getContacts();
assertThat(registrarPocs).hasSize(1);
RegistrarPoc registrarPoc = registrarPocs.stream().findAny().get();
assertThat(registrarPoc.getEmailAddress()).isEqualTo(email);
assertThat(registrarPoc.getName()).isEqualTo(email);
assertThat(registrarPoc.getLoginEmailAddress()).isEqualTo(email);
private void verifyIapPermission(@Nullable String emailAddress) {
if (emailAddress == null) {
cloudTasksHelper.assertNoTasksEnqueued("console-user-group-update");
verifyNoInteractions(iamClient);
} else {
String groupEmailAddress = command.maybeGroupEmailAddress.orElse(null);
if (groupEmailAddress == null) {
cloudTasksHelper.assertNoTasksEnqueued("console-user-group-update");
verify(iamClient).addBinding(emailAddress, IAP_SECURED_WEB_APP_USER_ROLE);
} else {
cloudTasksHelper.assertTasksEnqueued(
"console-user-group-update",
new TaskMatcher()
.service("TOOLS")
.method(HttpMethod.POST)
.path("/_dr/admin/updateUserGroup")
.param("userEmailAddress", emailAddress)
.param("groupEmailAddress", groupEmailAddress)
.param("groupUpdateMode", "ADD"));
verifyNoInteractions(iamClient);
}
}
}
@Test
@@ -136,10 +166,12 @@ class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
verifyRegistrarCreation("blobio-4", "blobio-ga", PASSWORD, ipAddress);
verifyRegistrarCreation("blobio-5", "blobio-eap", PASSWORD, ipAddress);
verifyRegistrarContactCreation("blobio-1", "contact@email.com");
verifyRegistrarContactCreation("blobio-3", "contact@email.com");
verifyRegistrarContactCreation("blobio-4", "contact@email.com");
verifyRegistrarContactCreation("blobio-5", "contact@email.com");
verifyUser("blobio-1", "contact@email.com");
verifyUser("blobio-3", "contact@email.com");
verifyUser("blobio-4", "contact@email.com");
verifyUser("blobio-5", "contact@email.com");
verifyIapPermission("contact@email.com");
}
@Test
@@ -162,10 +194,12 @@ class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
verifyRegistrarCreation("abc-4", "abc-ga", PASSWORD, ipAddress);
verifyRegistrarCreation("abc-5", "abc-eap", PASSWORD, ipAddress);
verifyRegistrarContactCreation("abc-1", "abc@email.com");
verifyRegistrarContactCreation("abc-3", "abc@email.com");
verifyRegistrarContactCreation("abc-4", "abc@email.com");
verifyRegistrarContactCreation("abc-5", "abc@email.com");
verifyUser("abc-1", "abc@email.com");
verifyUser("abc-3", "abc@email.com");
verifyUser("abc-4", "abc@email.com");
verifyUser("abc-5", "abc@email.com");
verifyIapPermission("abc@email.com");
}
@Test
@@ -180,19 +214,20 @@ class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
verifyTldCreation("blobio-ga", "BLOBIOG2", GENERAL_AVAILABILITY, false);
verifyTldCreation("blobio-eap", "BLOBIOE3", GENERAL_AVAILABILITY, true);
ImmutableList<CidrAddressBlock> ipAddresses = ImmutableList.of(
CidrAddressBlock.create("1.1.1.1"),
CidrAddressBlock.create("2.2.2.2"));
ImmutableList<CidrAddressBlock> ipAddresses =
ImmutableList.of(CidrAddressBlock.create("1.1.1.1"), CidrAddressBlock.create("2.2.2.2"));
verifyRegistrarCreation("blobio-1", "blobio-sunrise", PASSWORD, ipAddresses);
verifyRegistrarCreation("blobio-3", "blobio-ga", PASSWORD, ipAddresses);
verifyRegistrarCreation("blobio-4", "blobio-ga", PASSWORD, ipAddresses);
verifyRegistrarCreation("blobio-5", "blobio-eap", PASSWORD, ipAddresses);
verifyRegistrarContactCreation("blobio-1", "contact@email.com");
verifyRegistrarContactCreation("blobio-3", "contact@email.com");
verifyRegistrarContactCreation("blobio-4", "contact@email.com");
verifyRegistrarContactCreation("blobio-5", "contact@email.com");
verifyUser("blobio-1", "contact@email.com");
verifyUser("blobio-3", "contact@email.com");
verifyUser("blobio-4", "contact@email.com");
verifyUser("blobio-5", "contact@email.com");
verifyIapPermission("contact@email.com");
}
@Test
@@ -206,6 +241,7 @@ class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
"--email=contact@email.com",
"--certfile=" + getCertFilename()));
assertThat(thrown).hasMessageThat().contains("option is required: [-a | --ip_allow_list]");
verifyIapPermission(null);
}
@Test
@@ -219,6 +255,7 @@ class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
"--email=contact@email.com",
"--certfile=" + getCertFilename()));
assertThat(thrown).hasMessageThat().contains("option is required: [-r | --registrar]");
verifyIapPermission(null);
}
@Test
@@ -232,6 +269,7 @@ class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
assertThat(thrown)
.hasMessageThat()
.contains("Must specify exactly one client certificate file.");
verifyIapPermission(null);
}
@Test
@@ -245,6 +283,36 @@ class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
"--certfile=" + getCertFilename(),
"--registrar=blobio"));
assertThat(thrown).hasMessageThat().contains("option is required: [--email]");
verifyIapPermission(null);
}
@Test
void testSuccess_noConsoleUserGroup() throws Exception {
command.maybeGroupEmailAddress = Optional.empty();
runCommandForced(
"--ip_allow_list=1.1.1.1",
"--registrar=blobio",
"--email=contact@email.com",
"--certfile=" + getCertFilename());
verifyTldCreation("blobio-sunrise", "BLOBIOS0", START_DATE_SUNRISE, false);
verifyTldCreation("blobio-ga", "BLOBIOG2", GENERAL_AVAILABILITY, false);
verifyTldCreation("blobio-eap", "BLOBIOE3", GENERAL_AVAILABILITY, true);
ImmutableList<CidrAddressBlock> ipAddress =
ImmutableList.of(CidrAddressBlock.create("1.1.1.1"));
verifyRegistrarCreation("blobio-1", "blobio-sunrise", PASSWORD, ipAddress);
verifyRegistrarCreation("blobio-3", "blobio-ga", PASSWORD, ipAddress);
verifyRegistrarCreation("blobio-4", "blobio-ga", PASSWORD, ipAddress);
verifyRegistrarCreation("blobio-5", "blobio-eap", PASSWORD, ipAddress);
verifyUser("blobio-1", "contact@email.com");
verifyUser("blobio-3", "contact@email.com");
verifyUser("blobio-4", "contact@email.com");
verifyUser("blobio-5", "contact@email.com");
verifyIapPermission("contact@email.com");
}
@Test
@@ -259,6 +327,7 @@ class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
"--email=contact@email.com",
"--certfile=/dev/null"));
assertThat(thrown).hasMessageThat().contains("No X509Certificate found");
verifyIapPermission(null);
}
@Test
@@ -273,6 +342,7 @@ class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
"--email=contact@email.com",
"--certfile=" + getCertFilename()));
assertThat(thrown).hasMessageThat().contains("Invalid registrar name: 3blo-bio");
verifyIapPermission(null);
}
@Test
@@ -287,6 +357,7 @@ class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
"--email=contact@email.com",
"--certfile=" + getCertFilename()));
assertThat(thrown).hasMessageThat().contains("Invalid registrar name: bl");
verifyIapPermission(null);
}
@Test
@@ -301,6 +372,7 @@ class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
"--email=contact@email.com",
"--certfile=" + getCertFilename()));
assertThat(thrown).hasMessageThat().contains("Invalid registrar name: blobiotoooolong");
verifyIapPermission(null);
}
@Test
@@ -315,6 +387,7 @@ class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
"--email=contact@email.com",
"--certfile=" + getCertFilename()));
assertThat(thrown).hasMessageThat().contains("Invalid registrar name: blo#bio");
verifyIapPermission(null);
}
@Test
@@ -330,6 +403,7 @@ class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
"--email=contact@email.com",
"--certfile=" + getCertFilename()));
assertThat(thrown).hasMessageThat().contains("VKey<Tld>(sql:blobio-sunrise)");
verifyIapPermission(null);
}
@Test
@@ -345,6 +419,8 @@ class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
verifyTldCreation("blobio-sunrise", "BLOBIOS0", START_DATE_SUNRISE, false);
verifyTldCreation("blobio-ga", "BLOBIOG2", GENERAL_AVAILABILITY, false);
verifyIapPermission("contact@email.com");
}
@Test
@@ -366,6 +442,28 @@ class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
"--email=contact@email.com",
"--certfile=" + getCertFilename()));
assertThat(thrown).hasMessageThat().contains("VKey<Registrar>(sql:blobio-1)");
verifyIapPermission(null);
}
@Test
void testFailure_userExists() {
User user =
new User.Builder()
.setEmailAddress("contact@email.com")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build())
.build();
UserDao.saveUser(user);
IllegalStateException thrown =
assertThrows(
IllegalStateException.class,
() ->
runCommandForced(
"--ip_allow_list=1.1.1.1",
"--registrar=blobio",
"--email=contact@email.com",
"--certfile=" + getCertFilename()));
assertThat(thrown).hasMessageThat().contains("Found existing users: {contact@email.com");
verifyIapPermission(null);
}
@Test
@@ -385,10 +483,46 @@ class SetupOteCommandTest extends CommandTestCase<SetupOteCommand> {
"--email=contact@email.com",
"--certfile=" + getCertFilename());
ImmutableList<CidrAddressBlock> ipAddress = ImmutableList.of(
CidrAddressBlock.create("1.1.1.1"));
ImmutableList<CidrAddressBlock> ipAddress =
ImmutableList.of(CidrAddressBlock.create("1.1.1.1"));
verifyRegistrarCreation("blobio-1", "blobio-sunrise", PASSWORD, ipAddress);
verifyRegistrarCreation("blobio-3", "blobio-ga", PASSWORD, ipAddress);
verifyIapPermission("contact@email.com");
}
@Test
void testSuccess_userExists_replaceExisting() throws Exception {
User user =
new User.Builder()
.setEmailAddress("contact@email.com")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build())
.build();
UserDao.saveUser(user);
runCommandForced(
"--overwrite",
"--ip_allow_list=1.1.1.1",
"--registrar=blobio",
"--email=contact@email.com",
"--certfile=" + getCertFilename());
ImmutableList<CidrAddressBlock> ipAddress =
ImmutableList.of(CidrAddressBlock.create("1.1.1.1"));
verifyRegistrarCreation("blobio-1", "blobio-sunrise", PASSWORD, ipAddress);
verifyRegistrarCreation("blobio-3", "blobio-ga", PASSWORD, ipAddress);
verifyUser("blobio-1", "contact@email.com");
verifyUser("blobio-3", "contact@email.com");
verifyUser("blobio-4", "contact@email.com");
verifyUser("blobio-5", "contact@email.com");
// verify that the role is completely replaced, e.g., the global role is gone.
assertThat(UserDao.loadUser("contact@email.com").get().getUserRoles().getGlobalRole())
.isEqualTo(GlobalRole.NONE);
verifyIapPermission("contact@email.com");
}
}
@@ -40,6 +40,8 @@ import google.registry.model.domain.fee.FeeQueryCommandExtensionItem.CommandName
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.domain.token.AllocationToken.RegistrationBehavior;
import google.registry.model.domain.token.AllocationToken.TokenStatus;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.junit.jupiter.api.Test;
@@ -159,16 +161,22 @@ class UpdateAllocationTokensCommandTest extends CommandTestCase<UpdateAllocation
@Test
void testUpdateRenewalPriceBehavior_setToSpecified() throws Exception {
AllocationToken token = persistResource(builderWithPromo().setDiscountFraction(0.5).build());
runCommandForced("--prefix", "token", "--renewal_price_behavior", "SPECIFIED");
assertThat(reloadResource(token).getRenewalPriceBehavior()).isEqualTo(SPECIFIED);
AllocationToken token = persistResource(builderWithPromo().build());
runCommandForced(
"--prefix", "token", "--renewal_price_behavior", "SPECIFIED", "--renewal_price", "USD 1");
token = reloadResource(token);
assertThat(token.getRenewalPriceBehavior()).isEqualTo(SPECIFIED);
assertThat(token.getRenewalPrice()).hasValue(Money.of(CurrencyUnit.USD, 1));
}
@Test
void testUpdateRenewalPriceBehavior_setToDefault() throws Exception {
AllocationToken token =
persistResource(
builderWithPromo().setRenewalPriceBehavior(SPECIFIED).setDiscountFraction(0.5).build());
builderWithPromo()
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(CurrencyUnit.USD, 1))
.build());
runCommandForced("--prefix", "token", "--renewal_price_behavior", "default");
assertThat(reloadResource(token).getRenewalPriceBehavior()).isEqualTo(DEFAULT);
}
@@ -177,7 +185,10 @@ class UpdateAllocationTokensCommandTest extends CommandTestCase<UpdateAllocation
void testUpdateRenewalPriceBehavior_setToNonPremium() throws Exception {
AllocationToken token =
persistResource(
builderWithPromo().setRenewalPriceBehavior(SPECIFIED).setDiscountFraction(0.5).build());
builderWithPromo()
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(CurrencyUnit.USD, 1))
.build());
runCommandForced("--prefix", "token", "--renewal_price_behavior", "NONpremium");
assertThat(reloadResource(token).getRenewalPriceBehavior()).isEqualTo(NONPREMIUM);
}
@@ -193,11 +204,26 @@ class UpdateAllocationTokensCommandTest extends CommandTestCase<UpdateAllocation
void testUpdateRenewalPriceBehavior_setFromSpecifiedToSpecified() throws Exception {
AllocationToken token =
persistResource(
builderWithPromo().setRenewalPriceBehavior(SPECIFIED).setDiscountFraction(0.5).build());
builderWithPromo()
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(CurrencyUnit.USD, 1))
.build());
runCommandForced("--prefix", "token", "--renewal_price_behavior", "SPecified");
assertThat(reloadResource(token).getRenewalPriceBehavior()).isEqualTo(SPECIFIED);
}
@Test
void testFailure_nonSpecifiedToSpecified_withoutPrice() throws Exception {
persistResource(builderWithPromo().build());
assertThat(
assertThrows(
IllegalArgumentException.class,
() ->
runCommandForced("--prefix", "token", "--renewal_price_behavior", "SPECIFIED")))
.hasMessageThat()
.isEqualTo("renewalPrice must be specified iff renewalPriceBehavior is SPECIFIED");
}
@Test
void testUpdateRenewalPriceBehavior_setFromNonPremiumToDefault() throws Exception {
AllocationToken token =
@@ -214,7 +240,10 @@ class UpdateAllocationTokensCommandTest extends CommandTestCase<UpdateAllocation
void testUpdateRenewalPriceBehavior_setToMixedCaseDefault() throws Exception {
AllocationToken token =
persistResource(
builderWithPromo().setRenewalPriceBehavior(SPECIFIED).setDiscountFraction(0.5).build());
builderWithPromo()
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(CurrencyUnit.USD, 1))
.build());
runCommandForced("--prefix", "token", "--renewal_price_behavior", "deFauLt");
assertThat(reloadResource(token).getRenewalPriceBehavior()).isEqualTo(DEFAULT);
}
@@ -350,7 +379,9 @@ class UpdateAllocationTokensCommandTest extends CommandTestCase<UpdateAllocation
new AllocationToken.Builder()
.setToken("token")
.setTokenType(BULK_PRICING)
.setDiscountFraction(1.0)
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(CurrencyUnit.USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setTokenStatusTransitions(
@@ -377,7 +408,9 @@ class UpdateAllocationTokensCommandTest extends CommandTestCase<UpdateAllocation
new AllocationToken.Builder()
.setToken("token")
.setTokenType(BULK_PRICING)
.setDiscountFraction(1.0)
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(CurrencyUnit.USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setTokenStatusTransitions(
@@ -17,6 +17,7 @@ package google.registry.tools;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.persistResource;
import static org.joda.money.CurrencyUnit.USD;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableSet;
@@ -47,8 +48,9 @@ public class UpdateBulkPricingPackageCommandTest
.setAllowedTlds(ImmutableSet.of("foo"))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setDiscountFraction(1)
.setDiscountFraction(1.0)
.build());
BulkPricingPackage bulkPricingPackage =
new BulkPricingPackage.Builder()
@@ -94,8 +96,9 @@ public class UpdateBulkPricingPackageCommandTest
.setAllowedTlds(ImmutableSet.of("foo"))
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(USD, 0))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setDiscountFraction(1)
.setDiscountFraction(1.0)
.build());
IllegalArgumentException thrown =
assertThrows(
@@ -80,7 +80,7 @@ public class ConsoleRegistryLockActionTest {
Please click the link below to perform the lock / unlock action on domain example.test. \
Note: this code will expire in one hour.
https://registrarconsole.tld/console-api/registry-lock-verify?lockVerificationCode=\
https://registrarconsole.tld/console/#/registry-lock-verify?lockVerificationCode=\
123456789ABCDEFGHJKLMNPQRSTUVWXY""";
private static final Gson GSON = RequestModule.provideGson();
@@ -122,15 +122,7 @@ public class ConsoleRegistryLockActionTest {
@Test
void testGet_simpleLock() {
saveRegistryLock(
new RegistryLock.Builder()
.setRepoId("repoId")
.setDomainName("example.test")
.setRegistrarId("TheRegistrar")
.setVerificationCode("123456789ABCDEFGHJKLMNPQRSTUVWXY")
.setRegistrarPocId("johndoe@theregistrar.com")
.setLockCompletionTime(fakeClock.nowUtc())
.build());
saveRegistryLock(createDefaultLockBuilder().setLockCompletionTime(fakeClock.nowUtc()).build());
action.run();
assertThat(response.getStatus()).isEqualTo(SC_OK);
assertThat(response.getPayload())
@@ -0,0 +1,220 @@
// 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.ui.server.console;
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.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.SqlHelper.saveRegistryLock;
import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STATUSES;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import google.registry.model.console.RegistrarRole;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
import google.registry.model.domain.Domain;
import google.registry.model.domain.RegistryLock;
import google.registry.model.eppcommon.StatusValue;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.request.RequestModule;
import google.registry.request.auth.AuthResult;
import google.registry.testing.CloudTasksHelper;
import google.registry.testing.ConsoleApiParamsUtils;
import google.registry.testing.DeterministicStringGenerator;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeResponse;
import google.registry.tools.DomainLockUtils;
import google.registry.ui.server.registrar.ConsoleApiParams;
import google.registry.util.StringGenerator;
import jakarta.servlet.http.HttpServletResponse;
import org.joda.time.Duration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Tests for {@link ConsoleRegistryLockVerifyAction}. */
public class ConsoleRegistryLockVerifyActionTest {
private static final String DEFAULT_CODE = "123456789ABCDEFGHJKLMNPQRSTUUUUU";
private static final Gson GSON = RequestModule.provideGson();
private final FakeClock fakeClock = new FakeClock();
@RegisterExtension
final JpaTestExtensions.JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().withClock(fakeClock).buildIntegrationTestExtension();
private FakeResponse response;
private Domain defaultDomain;
private User user;
private ConsoleRegistryLockVerifyAction action;
@BeforeEach
void beforeEach() {
createTld("test");
defaultDomain = persistActiveDomain("example.test");
user =
new User.Builder()
.setEmailAddress("user@theregistrar.com")
.setRegistryLockEmailAddress("registrylock@theregistrar.com")
.setUserRoles(
new UserRoles.Builder()
.setRegistrarRoles(
ImmutableMap.of("TheRegistrar", RegistrarRole.PRIMARY_CONTACT))
.build())
.setRegistryLockPassword("registryLockPassword")
.build();
action = createAction(DEFAULT_CODE);
}
@Test
void testSuccess_lock() {
saveRegistryLock(createDefaultLockBuilder().build());
action.run();
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
assertThat(response.getPayload())
.isEqualTo(
"{\"action\":\"unlocked\",\"domainName\":\"example.test\",\"registrarId\":\"TheRegistrar\"}");
assertThat(loadByEntity(defaultDomain).getStatusValues())
.containsAtLeastElementsIn(REGISTRY_LOCK_STATUSES);
}
@Test
void testSuccess_unlock() {
persistResource(defaultDomain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build());
saveRegistryLock(
createDefaultLockBuilder()
.setLockCompletionTime(fakeClock.nowUtc())
.setUnlockRequestTime(fakeClock.nowUtc())
.build());
action.run();
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
assertThat(response.getPayload())
.isEqualTo(
"{\"action\":\"unlocked\",\"domainName\":\"example.test\",\"registrarId\":\"TheRegistrar\"}");
assertThat(loadByEntity(defaultDomain).getStatusValues()).containsExactly(StatusValue.INACTIVE);
}
@Test
void testSuccess_admin_lock() {
saveRegistryLock(createDefaultLockBuilder().isSuperuser(true).build());
user =
user.asBuilder()
.setUserRoles(user.getUserRoles().asBuilder().setIsAdmin(true).build())
.build();
action = createAction(DEFAULT_CODE);
action.run();
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
assertThat(response.getPayload())
.isEqualTo(
"{\"action\":\"unlocked\",\"domainName\":\"example.test\",\"registrarId\":\"TheRegistrar\"}");
assertThat(loadByEntity(defaultDomain).getStatusValues())
.containsAtLeastElementsIn(REGISTRY_LOCK_STATUSES);
}
@Test
void testSuccess_admin_unlock() {
persistResource(defaultDomain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build());
saveRegistryLock(
createDefaultLockBuilder()
.isSuperuser(true)
.setLockCompletionTime(fakeClock.nowUtc())
.setUnlockRequestTime(fakeClock.nowUtc())
.build());
user =
user.asBuilder()
.setUserRoles(user.getUserRoles().asBuilder().setIsAdmin(true).build())
.build();
action = createAction(DEFAULT_CODE);
action.run();
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
assertThat(response.getPayload())
.isEqualTo(
"{\"action\":\"unlocked\",\"domainName\":\"example.test\",\"registrarId\":\"TheRegistrar\"}");
assertThat(loadByEntity(defaultDomain).getStatusValues()).containsExactly(StatusValue.INACTIVE);
}
@Test
void testFailure_invalidCode() {
saveRegistryLock(createDefaultLockBuilder().setVerificationCode("foobar").build());
action.run();
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_BAD_REQUEST);
assertThat(response.getPayload())
.isEqualTo("Invalid verification code 123456789ABCDEFGHJKLMNPQRSTUUUUU");
assertThat(loadByEntity(defaultDomain).getStatusValues()).containsExactly(StatusValue.INACTIVE);
}
@Test
void testFailure_expiredLock() {
saveRegistryLock(createDefaultLockBuilder().build());
fakeClock.advanceBy(Duration.standardDays(1));
action.run();
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_BAD_REQUEST);
assertThat(response.getPayload()).isEqualTo("The pending lock has expired; please try again");
assertThat(loadByEntity(defaultDomain).getStatusValues()).containsExactly(StatusValue.INACTIVE);
}
@Test
void testFailure_nonAdmin_lock() {
saveRegistryLock(createDefaultLockBuilder().isSuperuser(true).build());
action.run();
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_BAD_REQUEST);
assertThat(response.getPayload()).isEqualTo("Non-admin user cannot complete admin lock");
assertThat(loadByEntity(defaultDomain).getStatusValues()).containsExactly(StatusValue.INACTIVE);
}
@Test
void testFailure_nonAdmin_unlock() {
persistResource(defaultDomain.asBuilder().setStatusValues(REGISTRY_LOCK_STATUSES).build());
saveRegistryLock(
createDefaultLockBuilder()
.isSuperuser(true)
.setLockCompletionTime(fakeClock.nowUtc())
.setUnlockRequestTime(fakeClock.nowUtc())
.build());
action.run();
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_BAD_REQUEST);
assertThat(response.getPayload()).isEqualTo("Non-admin user cannot complete admin unlock");
assertThat(loadByEntity(defaultDomain).getStatusValues())
.containsAtLeastElementsIn(REGISTRY_LOCK_STATUSES);
}
private RegistryLock.Builder createDefaultLockBuilder() {
return new RegistryLock.Builder()
.setRepoId(defaultDomain.getRepoId())
.setDomainName(defaultDomain.getDomainName())
.setRegistrarId(defaultDomain.getCurrentSponsorRegistrarId())
.setRegistrarPocId("johndoe@theregistrar.com")
.setVerificationCode(DEFAULT_CODE);
}
private ConsoleRegistryLockVerifyAction createAction(String verificationCode) {
AuthResult authResult = AuthResult.createUser(user);
ConsoleApiParams params = ConsoleApiParamsUtils.createFake(authResult);
when(params.request().getMethod()).thenReturn("GET");
when(params.request().getServerName()).thenReturn("registrarconsole.tld");
DomainLockUtils domainLockUtils =
new DomainLockUtils(
new DeterministicStringGenerator(StringGenerator.Alphabets.BASE_58),
"adminreg",
new CloudTasksHelper(fakeClock).getTestCloudTasksUtils());
response = (FakeResponse) params.response();
return new ConsoleRegistryLockVerifyAction(params, domainLockUtils, GSON, verificationCode);
}
}
@@ -15,11 +15,14 @@
package google.registry.ui.server.registrar;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.OteAccountBuilderTest.verifyIapPermission;
import static google.registry.model.OteAccountBuilderTest.verifyUser;
import static google.registry.model.registrar.Registrar.loadByRegistrarId;
import static google.registry.testing.DatabaseHelper.persistPremiumList;
import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import static org.joda.money.CurrencyUnit.USD;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import com.google.common.collect.ImmutableList;
@@ -35,10 +38,12 @@ import google.registry.request.Action.Method;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.AuthenticatedRegistrarAccessor;
import google.registry.security.XsrfTokenManager;
import google.registry.testing.CloudTasksHelper;
import google.registry.testing.DeterministicStringGenerator;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeResponse;
import google.registry.testing.SystemPropertyExtension;
import google.registry.tools.IamClient;
import google.registry.ui.server.SendEmailUtils;
import google.registry.util.EmailMessage;
import google.registry.util.RegistryEnvironment;
@@ -65,6 +70,8 @@ public final class ConsoleOteSetupActionTest {
@Order(value = Integer.MAX_VALUE)
final SystemPropertyExtension systemPropertyExtension = new SystemPropertyExtension();
private final IamClient iamClient = mock(IamClient.class);
private final CloudTasksHelper cloudTasksHelper = new CloudTasksHelper();
private final FakeResponse response = new FakeResponse();
private final ConsoleOteSetupAction action = new ConsoleOteSetupAction();
private final User user =
@@ -100,6 +107,9 @@ public final class ConsoleOteSetupActionTest {
action.optionalPassword = Optional.empty();
action.passwordGenerator = new DeterministicStringGenerator("abcdefghijklmnopqrstuvwxyz");
action.maybeGroupEmailAddress = Optional.of("group@example.com");
action.cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils();
action.iamClient = iamClient;
}
@Test
@@ -142,9 +152,7 @@ public final class ConsoleOteSetupActionTest {
// checking that all the entities are there or that they have the correct values.
assertThat(loadByRegistrarId("myclientid-3")).isPresent();
assertThat(Tld.get("myclientid-ga")).isNotNull();
assertThat(
loadByRegistrarId("myclientid-5").get().getContacts().asList().get(0).getEmailAddress())
.isEqualTo("contact@registry.example");
verifyUser("myclientid-5", "contact@registry.example");
assertThat(response.getPayload())
.contains("<h1>OT&E successfully created for registrar myclientid!</h1>");
ArgumentCaptor<EmailMessage> contentCaptor = ArgumentCaptor.forClass(EmailMessage.class);
@@ -163,6 +171,14 @@ public final class ConsoleOteSetupActionTest {
Gave user contact@registry.example web access to these Registrars
""");
assertThat(response.getPayload()).contains("gtag('config', 'sampleId')");
verifyIapPermission(
"contact@registry.example", action.maybeGroupEmailAddress, cloudTasksHelper, iamClient);
}
@Test
void testPost_authorized_noConsoleGroup() {
action.maybeGroupEmailAddress = Optional.empty();
testPost_authorized();
}
@Test
@@ -15,10 +15,13 @@
package google.registry.ui.server.registrar;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.OteAccountBuilderTest.verifyIapPermission;
import static google.registry.model.OteAccountBuilderTest.verifyUser;
import static google.registry.model.registrar.Registrar.loadByRegistrarId;
import static google.registry.testing.DatabaseHelper.persistPremiumList;
import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import static org.joda.money.CurrencyUnit.USD;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import com.google.common.collect.ImmutableList;
@@ -29,17 +32,18 @@ import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarAddress;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.request.Action.Method;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.AuthenticatedRegistrarAccessor;
import google.registry.security.XsrfTokenManager;
import google.registry.testing.CloudTasksHelper;
import google.registry.testing.DeterministicStringGenerator;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeResponse;
import google.registry.testing.SystemPropertyExtension;
import google.registry.tools.IamClient;
import google.registry.ui.server.SendEmailUtils;
import google.registry.util.EmailMessage;
import google.registry.util.RegistryEnvironment;
@@ -73,6 +77,8 @@ final class ConsoleRegistrarCreatorActionTest {
.setEmailAddress("marla.singer@example.com")
.setUserRoles(new UserRoles())
.build();
private final CloudTasksHelper cloudTasksHelper = new CloudTasksHelper();
private final IamClient iamClient = mock(IamClient.class);
@Mock HttpServletRequest request;
@Mock GmailClient gmailClient;
@@ -119,6 +125,10 @@ final class ConsoleRegistrarCreatorActionTest {
action.passcodeGenerator = new DeterministicStringGenerator("314159265");
action.analyticsConfig = ImmutableMap.of("googleAnalyticsId", "sampleId");
action.maybeGroupEmailAddress = Optional.of("group@example.com");
action.cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils();
action.iamClient = iamClient;
}
@Test
@@ -152,8 +162,7 @@ final class ConsoleRegistrarCreatorActionTest {
assertThat(response.getPayload()).contains("gtag('config', 'sampleId')");
}
@Test
void testPost_authorized_minimalAddress() {
void runTestPost_authorized_minimalAddress() {
action.clientId = Optional.of("myclientid");
action.name = Optional.of("registrar name");
action.billingAccount = Optional.of("USD=billing-account");
@@ -209,14 +218,22 @@ final class ConsoleRegistrarCreatorActionTest {
.setCountryCode("CC")
.build());
assertThat(registrar.getContacts())
.containsExactly(
new RegistrarPoc.Builder()
.setRegistrar(registrar)
.setName("myclientid@registry.example")
.setEmailAddress("myclientid@registry.example")
.setLoginEmailAddress("myclientid@registry.example")
.build());
verifyUser("myclientid", "myclientid@registry.example");
}
@Test
void testPost_authorized_minimalAddress_consoleUserGroup() {
runTestPost_authorized_minimalAddress();
verifyIapPermission(
"myclientid@registry.example", action.maybeGroupEmailAddress, cloudTasksHelper, iamClient);
}
@Test
void testPost_authorized_minimalAddress_NoConsoleUserGroup() {
action.maybeGroupEmailAddress = Optional.empty();
runTestPost_authorized_minimalAddress();
verifyIapPermission(
"myclientid@registry.example", action.maybeGroupEmailAddress, cloudTasksHelper, iamClient);
}
@Test
@@ -41,6 +41,7 @@ import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.FakeClock;
import google.registry.whois.WhoisResponse.WhoisResponseResults;
import java.util.Optional;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -264,7 +265,7 @@ class DomainWhoisResponseTest {
StatusValue.CLIENT_RENEW_PROHIBITED,
StatusValue.CLIENT_TRANSFER_PROHIBITED,
StatusValue.SERVER_UPDATE_PROHIBITED))
.setRegistrant(registrantResourceKey)
.setRegistrant(Optional.of(registrantResourceKey))
.setContacts(
ImmutableSet.of(
DesignatedContact.create(DesignatedContact.Type.ADMIN, adminResourceKey),
@@ -291,6 +292,40 @@ class DomainWhoisResponseTest {
.isEqualTo(WhoisResponseResults.create(loadFile("whois_domain.txt"), 1));
}
@Test
void getPlainTextOutputTest_noRegistrant() {
DomainWhoisResponse domainWhoisResponse =
new DomainWhoisResponse(
domain.asBuilder().setRegistrant(Optional.empty()).build(),
false,
"Please contact registrar",
clock.nowUtc());
assertThat(
domainWhoisResponse.getResponse(
false,
"Doodle Disclaimer\nI exist so that carriage return\nin disclaimer can be tested."))
.isEqualTo(WhoisResponseResults.create(loadFile("whois_domain_no_registrant.txt"), 1));
}
@Test
void getPlainTextOutputTest_noContacts() {
DomainWhoisResponse domainWhoisResponse =
new DomainWhoisResponse(
domain
.asBuilder()
.setRegistrant(Optional.empty())
.setContacts(ImmutableSet.of())
.build(),
false,
"Please contact registrar",
clock.nowUtc());
assertThat(
domainWhoisResponse.getResponse(
false,
"Doodle Disclaimer\nI exist so that carriage return\nin disclaimer can be tested."))
.isEqualTo(WhoisResponseResults.create(loadFile("whois_domain_no_contacts.txt"), 1));
}
@Test
void getPlainTextOutputTest_registrarAbuseInfoMissing() {
persistResource(abuseContact.asBuilder().setVisibleInDomainWhoisAsAbuse(false).build());
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<epp xmlns:domain="urn:ietf:params:xml:ns:domain-1.0" xmlns="urn:ietf:params:xml:ns:epp-1.0"
xmlns:fee12="urn:ietf:params:xml:ns:fee-0.12"
>
<response>
<result code="1000">
<msg>Command completed successfully</msg>
</result>
<resData>
<domain:creData>
<domain:name>rich.example</domain:name>
<domain:crDate>1999-04-03T22:00:00Z</domain:crDate>
<domain:exDate>2000-04-03T22:00:00Z</domain:exDate>
</domain:creData>
</resData>
<extension>
<fee12:creData>
<fee12:currency>USD</fee12:currency>
<fee12:fee description="create">13.00</fee12:fee>
</fee12:creData>
</extension>
<trID>
<clTRID>ABC-12345</clTRID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>
@@ -0,0 +1,35 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1000">
<msg>Command completed successfully</msg>
</result>
<resData>
<domain:infData
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>example.tld</domain:name>
<domain:roid>%ROID%</domain:roid>
<domain:status s="ok"/>
<domain:ns>
<domain:hostObj>ns1.example.tld</domain:hostObj>
<domain:hostObj>ns1.example.net</domain:hostObj>
</domain:ns>
<domain:host>ns1.example.tld</domain:host>
<domain:host>ns2.example.tld</domain:host>
<domain:clID>NewRegistrar</domain:clID>
<domain:crID>TheRegistrar</domain:crID>
<domain:crDate>1999-04-03T22:00:00.0Z</domain:crDate>
<domain:upID>NewRegistrar</domain:upID>
<domain:upDate>1999-12-03T09:00:00.0Z</domain:upDate>
<domain:exDate>2005-04-03T22:00:00.0Z</domain:exDate>
<domain:trDate>2000-04-08T09:00:00.0Z</domain:trDate>
<domain:authInfo>
<domain:pw>2fooBAR</domain:pw>
</domain:authInfo>
</domain:infData>
</resData>
<trID>
<clTRID>ABC-12345</clTRID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>
@@ -0,0 +1,37 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1000">
<msg>Command completed successfully</msg>
</result>
<resData>
<domain:infData
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>example.tld</domain:name>
<domain:roid>%ROID%</domain:roid>
<domain:status s="ok"/>
<domain:contact type="admin">sh8013</domain:contact>
<domain:contact type="tech">sh8013</domain:contact>
<domain:ns>
<domain:hostObj>ns1.example.tld</domain:hostObj>
<domain:hostObj>ns1.example.net</domain:hostObj>
</domain:ns>
<domain:host>ns1.example.tld</domain:host>
<domain:host>ns2.example.tld</domain:host>
<domain:clID>NewRegistrar</domain:clID>
<domain:crID>TheRegistrar</domain:crID>
<domain:crDate>1999-04-03T22:00:00.0Z</domain:crDate>
<domain:upID>NewRegistrar</domain:upID>
<domain:upDate>1999-12-03T09:00:00.0Z</domain:upDate>
<domain:exDate>2005-04-03T22:00:00.0Z</domain:exDate>
<domain:trDate>2000-04-08T09:00:00.0Z</domain:trDate>
<domain:authInfo>
<domain:pw>2fooBAR</domain:pw>
</domain:authInfo>
</domain:infData>
</resData>
<trID>
<clTRID>ABC-12345</clTRID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>
@@ -1,21 +1,22 @@
PATH CLASS METHODS OK MIN USER_POLICY
/_dr/epp EppTlsAction POST n APP ADMIN
/console-api/domain ConsoleDomainGetAction GET n USER PUBLIC
/console-api/domain-list ConsoleDomainListAction GET n USER PUBLIC
/console-api/dum-download ConsoleDumDownloadAction GET n USER PUBLIC
/console-api/eppPassword ConsoleEppPasswordAction POST n USER PUBLIC
/console-api/registrar ConsoleUpdateRegistrarAction POST n USER PUBLIC
/console-api/registrars RegistrarsAction GET,POST n USER PUBLIC
/console-api/registry-lock ConsoleRegistryLockAction GET,POST n USER PUBLIC
/console-api/settings/contacts ContactAction GET,POST n USER PUBLIC
/console-api/settings/security SecurityAction POST n USER PUBLIC
/console-api/settings/whois-fields WhoisRegistrarFieldsAction POST n USER PUBLIC
/console-api/userdata ConsoleUserDataAction GET n USER PUBLIC
/registrar ConsoleUiAction GET n USER PUBLIC
/registrar-create ConsoleRegistrarCreatorAction POST,GET n USER PUBLIC
/registrar-ote-setup ConsoleOteSetupAction POST,GET n USER PUBLIC
/registrar-ote-status OteStatusAction POST n USER PUBLIC
/registrar-settings RegistrarSettingsAction POST n USER PUBLIC
/registry-lock-get RegistryLockGetAction GET n USER PUBLIC
/registry-lock-post RegistryLockPostAction POST n USER PUBLIC
/registry-lock-verify RegistryLockVerifyAction GET n USER PUBLIC
PATH CLASS METHODS OK MIN USER_POLICY
/_dr/epp EppTlsAction POST n APP ADMIN
/console-api/domain ConsoleDomainGetAction GET n USER PUBLIC
/console-api/domain-list ConsoleDomainListAction GET n USER PUBLIC
/console-api/dum-download ConsoleDumDownloadAction GET n USER PUBLIC
/console-api/eppPassword ConsoleEppPasswordAction POST n USER PUBLIC
/console-api/registrar ConsoleUpdateRegistrarAction POST n USER PUBLIC
/console-api/registrars RegistrarsAction GET,POST n USER PUBLIC
/console-api/registry-lock ConsoleRegistryLockAction GET,POST n USER PUBLIC
/console-api/registry-lock-verify ConsoleRegistryLockVerifyAction GET n USER PUBLIC
/console-api/settings/contacts ContactAction GET,POST n USER PUBLIC
/console-api/settings/security SecurityAction POST n USER PUBLIC
/console-api/settings/whois-fields WhoisRegistrarFieldsAction POST n USER PUBLIC
/console-api/userdata ConsoleUserDataAction GET n USER PUBLIC
/registrar ConsoleUiAction GET n USER PUBLIC
/registrar-create ConsoleRegistrarCreatorAction POST,GET n USER PUBLIC
/registrar-ote-setup ConsoleOteSetupAction POST,GET n USER PUBLIC
/registrar-ote-status OteStatusAction POST n USER PUBLIC
/registrar-settings RegistrarSettingsAction POST n USER PUBLIC
/registry-lock-get RegistryLockGetAction GET n USER PUBLIC
/registry-lock-post RegistryLockPostAction POST n USER PUBLIC
/registry-lock-verify RegistryLockVerifyAction GET n USER PUBLIC
@@ -63,6 +63,7 @@ PATH CLASS
/console-api/registrar ConsoleUpdateRegistrarAction POST n USER PUBLIC
/console-api/registrars RegistrarsAction GET,POST n USER PUBLIC
/console-api/registry-lock ConsoleRegistryLockAction GET,POST n USER PUBLIC
/console-api/registry-lock-verify ConsoleRegistryLockVerifyAction GET n USER PUBLIC
/console-api/settings/contacts ContactAction GET,POST n USER PUBLIC
/console-api/settings/security SecurityAction POST n USER PUBLIC
/console-api/settings/whois-fields WhoisRegistrarFieldsAction POST n USER PUBLIC
@@ -0,0 +1,263 @@
{
"rdapConformance": [
"rdap_level_0",
"icann_rdap_response_profile_0",
"icann_rdap_technical_implementation_guide_0"
],
"objectClassName": "domain",
"handle": "%DOMAIN_HANDLE_1%",
"ldhName": "%DOMAIN_PUNYCODE_NAME_1%",
"status": [
"client delete prohibited",
"client renew prohibited",
"client transfer prohibited",
"server update prohibited"
],
"links": [
{
"href": "https://example.tld/rdap/domain/%DOMAIN_PUNYCODE_NAME_1%",
"type": "application/rdap+json",
"rel": "self"
},
{
"href": "https://rdap.example.com/withSlash/domain/%DOMAIN_PUNYCODE_NAME_1%",
"type": "application/rdap+json",
"rel": "related"
},
{
"href": "https://rdap.example.com/withoutSlash/domain/%DOMAIN_PUNYCODE_NAME_1%",
"type": "application/rdap+json",
"rel": "related"
}
],
"events": [
{
"eventAction": "registration",
"eventActor": "TheRegistrar",
"eventDate": "1997-01-01T00:00:00.000Z"
},
{
"eventAction": "expiration",
"eventDate": "2110-10-08T00:44:59.000Z"
},
{
"eventAction": "last update of RDAP database",
"eventDate": "2000-01-01T00:00:00.000Z"
},
{
"eventAction": "last changed",
"eventDate": "2009-05-29T20:13:00.000Z"
}
],
"nameservers": [
{
"objectClassName": "nameserver",
"handle": "%NAMESERVER_HANDLE_1%",
"ldhName": "%NAMESERVER_NAME_1%",
"links": [
{
"href": "https://example.tld/rdap/nameserver/%NAMESERVER_NAME_1%",
"type": "application/rdap+json",
"rel": "self"
}
],
"remarks": [
{
"title": "Incomplete Data",
"type": "object truncated due to unexplainable reasons",
"description": ["Summary data only. For complete data, send a specific query for the object."]
}
]
},
{
"objectClassName": "nameserver",
"handle": "%NAMESERVER_HANDLE_2%",
"ldhName": "%NAMESERVER_NAME_2%",
"links": [
{
"href": "https://example.tld/rdap/nameserver/%NAMESERVER_NAME_2%",
"type": "application/rdap+json",
"rel": "self"
}
],
"remarks": [
{
"title": "Incomplete Data",
"type": "object truncated due to unexplainable reasons",
"description": ["Summary data only. For complete data, send a specific query for the object."]
}
]
}
],
"secureDNS" : {
"delegationSigned": true,
"zoneSigned":true,
"dsData":[
{"algorithm":2,"digest":"DEADFACE","digestType":3,"keyTag":1}
]
},
"entities": [
{
"objectClassName" : "entity",
"handle" : "1",
"roles" : ["registrar"],
"links" : [
{
"rel" : "self",
"href" : "https://example.tld/rdap/entity/1",
"type" : "application/rdap+json"
}
],
"publicIds" : [
{
"type" : "IANA Registrar ID",
"identifier" : "1"
}
],
"vcardArray" : [
"vcard",
[
["version", {}, "text", "4.0"],
["fn", {}, "text", "%REGISTRAR_FULL_NAME_1%"]
]
],
"entities" : [
{
"objectClassName":"entity",
"roles":["abuse"],
"status":["active"],
"vcardArray": [
"vcard",
[
["version",{},"text","4.0"],
["fn",{},"text","Jake Doe"],
["tel",{"type":["voice"]},"uri","tel:+1.2125551216"],
["tel",{"type":["fax"]},"uri","tel:+1.2125551216"],
["email",{},"text","jakedoe@example.com"]
]
]
}
],
"remarks": [
{
"title": "Incomplete Data",
"description": [
"Summary data only. For complete data, send a specific query for the object."
],
"type": "object truncated due to unexplainable reasons"
}
]
},
{
"objectClassName":"entity",
"handle":"",
"remarks":[
{
"title":"REDACTED FOR PRIVACY",
"type":"object redacted due to authorization",
"description":[
"Some of the data in this object has been removed.",
"Contact personal data is visible only to the owning registrar."
],
"links":[
{
"href":"https://github.com/google/nomulus/blob/master/docs/rdap.md#authentication",
"rel":"alternate",
"type":"text/html"
}
]
},
{
"title":"EMAIL REDACTED FOR PRIVACY",
"type":"object redacted due to authorization",
"description":[
"Please query the RDDS service of the Registrar of Record identifies in this output for information on how to contact the Registrant of the queried domain name."
]
}
],
"roles":["registrant"],
"vcardArray":[
"vcard",
[
["version", {}, "text", "4.0"],
["fn", {}, "text", ""]
]
]
},
{
"objectClassName": "entity",
"handle": "",
"roles":["administrative"],
"remarks": [
{
"title":"REDACTED FOR PRIVACY",
"type":"object redacted due to authorization",
"description": [
"Some of the data in this object has been removed.",
"Contact personal data is visible only to the owning registrar."
],
"links":[
{
"href":"https://github.com/google/nomulus/blob/master/docs/rdap.md#authentication",
"rel":"alternate",
"type":"text/html"
}
]
},
{
"title":"EMAIL REDACTED FOR PRIVACY",
"type":"object redacted due to authorization",
"description": [
"Please query the RDDS service of the Registrar of Record identifies in this output for information on how to contact the Registrant of the queried domain name."
]
}
],
"vcardArray":[
"vcard",
[
["version", {}, "text", "4.0"],
["fn", {}, "text", ""]
]
]
},
{
"objectClassName":"entity",
"handle":"",
"remarks":[
{
"title":"REDACTED FOR PRIVACY",
"type":"object redacted due to authorization",
"description":[
"Some of the data in this object has been removed.",
"Contact personal data is visible only to the owning registrar."
],
"links":[
{
"href":"https://github.com/google/nomulus/blob/master/docs/rdap.md#authentication",
"rel":"alternate",
"type":"text/html"
}
]
},
{
"description":[
"Please query the RDDS service of the Registrar of Record identifies in this output for information on how to contact the Registrant of the queried domain name."
],
"title":"EMAIL REDACTED FOR PRIVACY",
"type":"object redacted due to authorization"
}
],
"roles": ["technical"],
"vcardArray": [
"vcard",
[
["version", {}, "text", "4.0"],
["fn", {}, "text", ""]
]
]
}
]
}
@@ -0,0 +1,4 @@
featureName: "TEST_FEATURE"
status:
"1970-01-01T00:00:00.000Z": "INACTIVE"
"1985-02-15T06:07:08.789Z": "ACTIVE"

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