mirror of
https://github.com/google/nomulus
synced 2026-09-19 22:44:26 +00:00
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
This commit is contained in:
@@ -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.YAMLFactory;
|
||||||
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature;
|
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature;
|
||||||
import com.google.common.collect.ImmutableSortedSet;
|
import com.google.common.collect.ImmutableSortedSet;
|
||||||
|
import google.registry.model.common.FeatureFlag.FeatureStatus;
|
||||||
import google.registry.model.common.TimedTransitionProperty;
|
import google.registry.model.common.TimedTransitionProperty;
|
||||||
import google.registry.model.domain.token.AllocationToken;
|
import google.registry.model.domain.token.AllocationToken;
|
||||||
import google.registry.model.tld.Tld.TldState;
|
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}. */
|
/** A custom JSON deserializer for a {@link CreateAutoTimestamp}. */
|
||||||
public static class CreateAutoTimestampDeserializer extends StdDeserializer<CreateAutoTimestamp> {
|
public static class CreateAutoTimestampDeserializer extends StdDeserializer<CreateAutoTimestamp> {
|
||||||
|
|
||||||
|
|||||||
@@ -14,29 +14,34 @@
|
|||||||
|
|
||||||
package google.registry.model.common;
|
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.base.Preconditions.checkArgument;
|
||||||
import static com.google.common.collect.ImmutableMap.toImmutableMap;
|
import static com.google.common.collect.ImmutableMap.toImmutableMap;
|
||||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||||
import static google.registry.config.RegistryConfig.getSingletonCacheRefreshDuration;
|
import static google.registry.config.RegistryConfig.getSingletonCacheRefreshDuration;
|
||||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
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.CacheLoader;
|
||||||
import com.github.benmanes.caffeine.cache.LoadingCache;
|
import com.github.benmanes.caffeine.cache.LoadingCache;
|
||||||
import com.google.common.base.Joiner;
|
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.ImmutableMap;
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import com.google.common.collect.ImmutableSortedMap;
|
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.Buildable;
|
||||||
import google.registry.model.CacheUtils;
|
import google.registry.model.CacheUtils;
|
||||||
|
import google.registry.model.EntityYamlUtils.TimedTransitionPropertyFeatureStatusDeserializer;
|
||||||
import google.registry.model.ImmutableObject;
|
import google.registry.model.ImmutableObject;
|
||||||
import google.registry.persistence.VKey;
|
import google.registry.persistence.VKey;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import javax.persistence.Column;
|
import javax.persistence.Column;
|
||||||
import javax.persistence.Entity;
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.EnumType;
|
||||||
|
import javax.persistence.Enumerated;
|
||||||
import javax.persistence.Id;
|
import javax.persistence.Id;
|
||||||
import org.joda.time.DateTime;
|
import org.joda.time.DateTime;
|
||||||
|
|
||||||
@@ -54,60 +59,76 @@ public class FeatureFlag extends ImmutableObject implements Buildable {
|
|||||||
INACTIVE
|
INACTIVE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum FeatureName {
|
||||||
|
TEST_FEATURE,
|
||||||
|
MINIMUM_DATASET_CONTACTS_OPTIONAL,
|
||||||
|
MINIMUM_DATASET_CONTACTS_PROHIBITED
|
||||||
|
}
|
||||||
|
|
||||||
/** The name of the flag/feature. */
|
/** 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. */
|
/** A map of times for each {@link FeatureStatus} the FeatureFlag should hold. */
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
|
@JsonDeserialize(using = TimedTransitionPropertyFeatureStatusDeserializer.class)
|
||||||
TimedTransitionProperty<FeatureStatus> status =
|
TimedTransitionProperty<FeatureStatus> status =
|
||||||
TimedTransitionProperty.withInitialValue(FeatureStatus.INACTIVE);
|
TimedTransitionProperty.withInitialValue(FeatureStatus.INACTIVE);
|
||||||
|
|
||||||
public static FeatureFlag get(String featureName) {
|
public static Optional<FeatureFlag> getUncached(FeatureName featureName) {
|
||||||
FeatureFlag maybeFeatureFlag = CACHE.get(featureName);
|
return tm().transact(() -> tm().loadByKeyIfPresent(createVKey(featureName)));
|
||||||
if (maybeFeatureFlag == null) {
|
|
||||||
throw new FeatureFlagNotFoundException(featureName);
|
|
||||||
} else {
|
|
||||||
return maybeFeatureFlag;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ImmutableSet<FeatureFlag> get(Set<String> featureNames) {
|
public static ImmutableList<FeatureFlag> getAllUncached() {
|
||||||
Map<String, FeatureFlag> featureFlags = CACHE.getAll(featureNames);
|
return tm().transact(() -> tm().loadAllOf(FeatureFlag.class));
|
||||||
ImmutableSet<String> missingFlags =
|
}
|
||||||
Sets.difference(featureNames, featureFlags.keySet()).immutableCopy();
|
|
||||||
|
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()) {
|
if (missingFlags.isEmpty()) {
|
||||||
return featureFlags.values().stream().collect(toImmutableSet());
|
return featureFlags.values().stream().map(Optional::get).collect(toImmutableSet());
|
||||||
} else {
|
} else {
|
||||||
throw new FeatureFlagNotFoundException(missingFlags);
|
throw new FeatureFlagNotFoundException(missingFlags);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A cache that loads the {@link FeatureFlag} for a given featureName. */
|
/** 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())
|
CacheUtils.newCacheBuilder(getSingletonCacheRefreshDuration())
|
||||||
.build(
|
.build(
|
||||||
new CacheLoader<>() {
|
new CacheLoader<>() {
|
||||||
@Override
|
@Override
|
||||||
public FeatureFlag load(final String featureName) {
|
public Optional<FeatureFlag> load(final FeatureName featureName) {
|
||||||
return tm().reTransact(() -> tm().loadByKeyIfPresent(createVKey(featureName)))
|
return tm().reTransact(() -> tm().loadByKeyIfPresent(createVKey(featureName)));
|
||||||
.orElse(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<? extends String, ? extends FeatureFlag> loadAll(
|
public Map<? extends FeatureName, ? extends Optional<FeatureFlag>> loadAll(
|
||||||
Set<? extends String> featureFlagNames) {
|
Set<? extends FeatureName> featureFlagNames) {
|
||||||
ImmutableMap<String, VKey<FeatureFlag>> keysMap =
|
ImmutableMap<FeatureName, VKey<FeatureFlag>> keysMap =
|
||||||
featureFlagNames.stream()
|
featureFlagNames.stream()
|
||||||
.collect(
|
.collect(
|
||||||
toImmutableMap(featureName -> featureName, FeatureFlag::createVKey));
|
toImmutableMap(featureName -> featureName, FeatureFlag::createVKey));
|
||||||
Map<VKey<? extends FeatureFlag>, FeatureFlag> entities =
|
Map<VKey<? extends FeatureFlag>, FeatureFlag> entities =
|
||||||
tm().reTransact(() -> tm().loadByKeysIfPresent(keysMap.values()));
|
tm().reTransact(() -> tm().loadByKeysIfPresent(keysMap.values()));
|
||||||
return entities.values().stream()
|
return Maps.toMap(
|
||||||
.collect(toImmutableMap(flag -> flag.featureName, flag -> flag));
|
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);
|
return VKey.create(FeatureFlag.class, featureName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,10 +137,11 @@ public class FeatureFlag extends ImmutableObject implements Buildable {
|
|||||||
return createVKey(featureName);
|
return createVKey(featureName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getFeatureName() {
|
public FeatureName getFeatureName() {
|
||||||
return featureName;
|
return featureName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonProperty("status")
|
||||||
public TimedTransitionProperty<FeatureStatus> getStatusMap() {
|
public TimedTransitionProperty<FeatureStatus> getStatusMap() {
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
@@ -144,20 +166,17 @@ public class FeatureFlag extends ImmutableObject implements Buildable {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public FeatureFlag build() {
|
public FeatureFlag build() {
|
||||||
checkArgument(
|
|
||||||
!Strings.isNullOrEmpty(getInstance().featureName),
|
|
||||||
"Feature name must not be null or empty");
|
|
||||||
getInstance().status.checkValidity();
|
getInstance().status.checkValidity();
|
||||||
|
checkArgument(getInstance().featureName != null, "FeatureName cannot be null");
|
||||||
return super.build();
|
return super.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder setFeatureName(String featureName) {
|
public Builder setFeatureName(FeatureName featureName) {
|
||||||
checkState(getInstance().featureName == null, "Feature name can only be set once");
|
|
||||||
getInstance().featureName = featureName;
|
getInstance().featureName = featureName;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder setStatus(ImmutableSortedMap<DateTime, FeatureStatus> statusMap) {
|
public Builder setStatusMap(ImmutableSortedMap<DateTime, FeatureStatus> statusMap) {
|
||||||
getInstance().status = TimedTransitionProperty.fromValueMap(statusMap);
|
getInstance().status = TimedTransitionProperty.fromValueMap(statusMap);
|
||||||
return this;
|
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). */
|
/** Exception to throw when no FeatureFlag entity is found for given FeatureName string(s). */
|
||||||
public static class FeatureFlagNotFoundException extends RuntimeException {
|
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));
|
super("No feature flag object(s) found for " + Joiner.on(", ").join(featureNames));
|
||||||
}
|
}
|
||||||
|
|
||||||
FeatureFlagNotFoundException(String featureName) {
|
public FeatureFlagNotFoundException(FeatureName featureName) {
|
||||||
this(ImmutableSet.of(featureName));
|
this(ImmutableSet.of(featureName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,9 +41,9 @@ import com.google.common.collect.ImmutableList;
|
|||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import com.google.common.collect.ImmutableSortedMap;
|
import com.google.common.collect.ImmutableSortedMap;
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
import com.google.common.collect.Ordering;
|
import com.google.common.collect.Ordering;
|
||||||
import com.google.common.collect.Range;
|
import com.google.common.collect.Range;
|
||||||
import com.google.common.collect.Sets;
|
|
||||||
import com.google.common.net.InternetDomainName;
|
import com.google.common.net.InternetDomainName;
|
||||||
import google.registry.model.Buildable;
|
import google.registry.model.Buildable;
|
||||||
import google.registry.model.CacheUtils;
|
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. */
|
/** Returns the TLD for a given TLD, throwing if none exists. */
|
||||||
public static Tld get(String tld) {
|
public static Tld get(String tld) {
|
||||||
Tld maybeTld = CACHE.get(tld);
|
return CACHE.get(tld).orElseThrow(() -> new TldNotFoundException(tld));
|
||||||
if (maybeTld == null) {
|
|
||||||
throw new TldNotFoundException(tld);
|
|
||||||
} else {
|
|
||||||
return maybeTld;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the TLD entities for the given TLD strings, throwing if any don't exist. */
|
/** Returns the TLD entities for the given TLD strings, throwing if any don't exist. */
|
||||||
public static ImmutableSet<Tld> get(Set<String> tlds) {
|
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 =
|
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()) {
|
if (missingRegistries.isEmpty()) {
|
||||||
return registries.values().stream().collect(toImmutableSet());
|
return registries.values().stream().map(Optional::get).collect(toImmutableSet());
|
||||||
} else {
|
} else {
|
||||||
throw new TldNotFoundException(missingRegistries);
|
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. */
|
/** 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())
|
CacheUtils.newCacheBuilder(getSingletonCacheRefreshDuration())
|
||||||
.build(
|
.build(
|
||||||
new CacheLoader<>() {
|
new CacheLoader<>() {
|
||||||
@Override
|
@Override
|
||||||
public Tld load(final String tld) {
|
public Optional<Tld> load(final String tld) {
|
||||||
return tm().reTransact(() -> tm().loadByKeyIfPresent(createVKey(tld)))
|
return tm().reTransact(() -> tm().loadByKeyIfPresent(createVKey(tld)));
|
||||||
.orElse(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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 =
|
ImmutableMap<String, VKey<Tld>> keysMap =
|
||||||
tlds.stream().collect(toImmutableMap(tld -> tld, Tld::createVKey));
|
tlds.stream().collect(toImmutableMap(tld -> tld, Tld::createVKey));
|
||||||
Map<VKey<? extends Tld>, Tld> entities =
|
Map<VKey<? extends Tld>, Tld> entities =
|
||||||
tm().reTransact(() -> tm().loadByKeysIfPresent(keysMap.values()));
|
tm().reTransact(() -> tm().loadByKeysIfPresent(keysMap.values()));
|
||||||
return entities.values().stream()
|
return Maps.toMap(
|
||||||
.collect(toImmutableMap(tld -> tld.tldStr, tld -> tld));
|
tlds, tld -> Optional.ofNullable(entities.get(createVKey(tld))));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -111,8 +111,8 @@ abstract class CreateOrUpdateBulkPricingPackageCommand extends MutatingCommand {
|
|||||||
if (clearLastNotificationSent()) {
|
if (clearLastNotificationSent()) {
|
||||||
builder.setLastNotificationSent(null);
|
builder.setLastNotificationSent(null);
|
||||||
}
|
}
|
||||||
BulkPricingPackage newBUlkPricingPackage = builder.build();
|
BulkPricingPackage newBulkPricingPackage = builder.build();
|
||||||
stageEntityChange(oldBulkPricingPackage, newBUlkPricingPackage);
|
stageEntityChange(oldBulkPricingPackage, newBulkPricingPackage);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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("canonicalize_labels", CanonicalizeLabelsCommand.class)
|
||||||
.put("check_domain", CheckDomainCommand.class)
|
.put("check_domain", CheckDomainCommand.class)
|
||||||
.put("check_domain_claims", CheckDomainClaimsCommand.class)
|
.put("check_domain_claims", CheckDomainClaimsCommand.class)
|
||||||
|
.put("configure_feature_flag", ConfigureFeatureFlagCommand.class)
|
||||||
.put("configure_tld", ConfigureTldCommand.class)
|
.put("configure_tld", ConfigureTldCommand.class)
|
||||||
.put("convert_idn", ConvertIdnCommand.class)
|
.put("convert_idn", ConvertIdnCommand.class)
|
||||||
.put("count_domains", CountDomainsCommand.class)
|
.put("count_domains", CountDomainsCommand.class)
|
||||||
@@ -71,6 +72,7 @@ public final class RegistryTool {
|
|||||||
.put("get_claims_list", GetClaimsListCommand.class)
|
.put("get_claims_list", GetClaimsListCommand.class)
|
||||||
.put("get_contact", GetContactCommand.class)
|
.put("get_contact", GetContactCommand.class)
|
||||||
.put("get_domain", GetDomainCommand.class)
|
.put("get_domain", GetDomainCommand.class)
|
||||||
|
.put("get_feature_flag", GetFeatureFlagCommand.class)
|
||||||
.put("get_history_entries", GetHistoryEntriesCommand.class)
|
.put("get_history_entries", GetHistoryEntriesCommand.class)
|
||||||
.put("get_host", GetHostCommand.class)
|
.put("get_host", GetHostCommand.class)
|
||||||
.put("get_keyring_secret", GetKeyringSecretCommand.class)
|
.put("get_keyring_secret", GetKeyringSecretCommand.class)
|
||||||
@@ -85,6 +87,7 @@ public final class RegistryTool {
|
|||||||
.put("hash_certificate", HashCertificateCommand.class)
|
.put("hash_certificate", HashCertificateCommand.class)
|
||||||
.put("list_cursors", ListCursorsCommand.class)
|
.put("list_cursors", ListCursorsCommand.class)
|
||||||
.put("list_domains", ListDomainsCommand.class)
|
.put("list_domains", ListDomainsCommand.class)
|
||||||
|
.put("list_feature_flags", ListFeatureFlagsCommand.class)
|
||||||
.put("list_hosts", ListHostsCommand.class)
|
.put("list_hosts", ListHostsCommand.class)
|
||||||
.put("list_premium_lists", ListPremiumListsCommand.class)
|
.put("list_premium_lists", ListPremiumListsCommand.class)
|
||||||
.put("list_registrars", ListRegistrarsCommand.class)
|
.put("list_registrars", ListRegistrarsCommand.class)
|
||||||
|
|||||||
@@ -119,6 +119,8 @@ interface RegistryToolComponent {
|
|||||||
|
|
||||||
void inject(GetDomainCommand command);
|
void inject(GetDomainCommand command);
|
||||||
|
|
||||||
|
void inject(GetFeatureFlagCommand command);
|
||||||
|
|
||||||
void inject(GetHostCommand command);
|
void inject(GetHostCommand command);
|
||||||
|
|
||||||
void inject(GetKeyringSecretCommand command);
|
void inject(GetKeyringSecretCommand command);
|
||||||
@@ -131,6 +133,8 @@ interface RegistryToolComponent {
|
|||||||
|
|
||||||
void inject(ListCursorsCommand command);
|
void inject(ListCursorsCommand command);
|
||||||
|
|
||||||
|
void inject(ListFeatureFlagsCommand command);
|
||||||
|
|
||||||
void inject(LockDomainCommand command);
|
void inject(LockDomainCommand command);
|
||||||
|
|
||||||
void inject(LoginCommand command);
|
void inject(LoginCommand command);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkArgument;
|
|||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import com.google.common.collect.ImmutableSortedMap;
|
import com.google.common.collect.ImmutableSortedMap;
|
||||||
import com.google.common.collect.Ordering;
|
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.domain.token.AllocationToken.TokenStatus;
|
||||||
import google.registry.model.tld.Tld.TldState;
|
import google.registry.model.tld.Tld.TldState;
|
||||||
import org.joda.money.Money;
|
import org.joda.money.Money;
|
||||||
@@ -72,4 +73,12 @@ public abstract class TransitionListParameter<V> extends KeyValueMapParameter<Da
|
|||||||
return TokenStatus.valueOf(value);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
package google.registry.model.common;
|
package google.registry.model.common;
|
||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
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.ACTIVE;
|
||||||
import static google.registry.model.common.FeatureFlag.FeatureStatus.INACTIVE;
|
import static google.registry.model.common.FeatureFlag.FeatureStatus.INACTIVE;
|
||||||
import static google.registry.testing.DatabaseHelper.loadByEntity;
|
import static google.registry.testing.DatabaseHelper.loadByEntity;
|
||||||
@@ -42,8 +45,8 @@ public class FeatureFlagTest extends EntityTestCase {
|
|||||||
void testSuccess_persistence() {
|
void testSuccess_persistence() {
|
||||||
FeatureFlag featureFlag =
|
FeatureFlag featureFlag =
|
||||||
new FeatureFlag.Builder()
|
new FeatureFlag.Builder()
|
||||||
.setFeatureName("testFlag")
|
.setFeatureName(TEST_FEATURE)
|
||||||
.setStatus(
|
.setStatusMap(
|
||||||
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
|
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
|
||||||
.put(START_OF_TIME, INACTIVE)
|
.put(START_OF_TIME, INACTIVE)
|
||||||
.put(DateTime.now(UTC).plusWeeks(8), ACTIVE)
|
.put(DateTime.now(UTC).plusWeeks(8), ACTIVE)
|
||||||
@@ -58,15 +61,15 @@ public class FeatureFlagTest extends EntityTestCase {
|
|||||||
void testSuccess_getSingleFlag() {
|
void testSuccess_getSingleFlag() {
|
||||||
FeatureFlag featureFlag =
|
FeatureFlag featureFlag =
|
||||||
new FeatureFlag.Builder()
|
new FeatureFlag.Builder()
|
||||||
.setFeatureName("testFlag")
|
.setFeatureName(TEST_FEATURE)
|
||||||
.setStatus(
|
.setStatusMap(
|
||||||
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
|
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
|
||||||
.put(START_OF_TIME, INACTIVE)
|
.put(START_OF_TIME, INACTIVE)
|
||||||
.put(DateTime.now(UTC).plusWeeks(8), ACTIVE)
|
.put(DateTime.now(UTC).plusWeeks(8), ACTIVE)
|
||||||
.build())
|
.build())
|
||||||
.build();
|
.build();
|
||||||
persistResource(featureFlag);
|
persistResource(featureFlag);
|
||||||
assertThat(FeatureFlag.get("testFlag")).isEqualTo(featureFlag);
|
assertThat(FeatureFlag.get(TEST_FEATURE)).isEqualTo(featureFlag);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -74,8 +77,8 @@ public class FeatureFlagTest extends EntityTestCase {
|
|||||||
FeatureFlag featureFlag1 =
|
FeatureFlag featureFlag1 =
|
||||||
persistResource(
|
persistResource(
|
||||||
new FeatureFlag.Builder()
|
new FeatureFlag.Builder()
|
||||||
.setFeatureName("testFlag1")
|
.setFeatureName(TEST_FEATURE)
|
||||||
.setStatus(
|
.setStatusMap(
|
||||||
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
|
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
|
||||||
.put(START_OF_TIME, INACTIVE)
|
.put(START_OF_TIME, INACTIVE)
|
||||||
.put(DateTime.now(UTC).plusWeeks(8), ACTIVE)
|
.put(DateTime.now(UTC).plusWeeks(8), ACTIVE)
|
||||||
@@ -84,8 +87,8 @@ public class FeatureFlagTest extends EntityTestCase {
|
|||||||
FeatureFlag featureFlag2 =
|
FeatureFlag featureFlag2 =
|
||||||
persistResource(
|
persistResource(
|
||||||
new FeatureFlag.Builder()
|
new FeatureFlag.Builder()
|
||||||
.setFeatureName("testFlag2")
|
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
|
||||||
.setStatus(
|
.setStatusMap(
|
||||||
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
|
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
|
||||||
.put(START_OF_TIME, INACTIVE)
|
.put(START_OF_TIME, INACTIVE)
|
||||||
.put(DateTime.now(UTC).plusWeeks(3), INACTIVE)
|
.put(DateTime.now(UTC).plusWeeks(3), INACTIVE)
|
||||||
@@ -94,14 +97,18 @@ public class FeatureFlagTest extends EntityTestCase {
|
|||||||
FeatureFlag featureFlag3 =
|
FeatureFlag featureFlag3 =
|
||||||
persistResource(
|
persistResource(
|
||||||
new FeatureFlag.Builder()
|
new FeatureFlag.Builder()
|
||||||
.setFeatureName("testFlag3")
|
.setFeatureName(MINIMUM_DATASET_CONTACTS_PROHIBITED)
|
||||||
.setStatus(
|
.setStatusMap(
|
||||||
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
|
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
|
||||||
.put(START_OF_TIME, INACTIVE)
|
.put(START_OF_TIME, INACTIVE)
|
||||||
.build())
|
.build())
|
||||||
.build());
|
.build());
|
||||||
ImmutableSet<FeatureFlag> featureFlags =
|
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.size()).isEqualTo(3);
|
||||||
assertThat(featureFlags).containsExactly(featureFlag1, featureFlag2, featureFlag3);
|
assertThat(featureFlags).containsExactly(featureFlag1, featureFlag2, featureFlag3);
|
||||||
}
|
}
|
||||||
@@ -110,8 +117,8 @@ public class FeatureFlagTest extends EntityTestCase {
|
|||||||
void testFailure_getMultipleFlagsOneMissing() {
|
void testFailure_getMultipleFlagsOneMissing() {
|
||||||
persistResource(
|
persistResource(
|
||||||
new FeatureFlag.Builder()
|
new FeatureFlag.Builder()
|
||||||
.setFeatureName("testFlag1")
|
.setFeatureName(TEST_FEATURE)
|
||||||
.setStatus(
|
.setStatusMap(
|
||||||
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
|
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
|
||||||
.put(START_OF_TIME, INACTIVE)
|
.put(START_OF_TIME, INACTIVE)
|
||||||
.put(DateTime.now(UTC).plusWeeks(8), ACTIVE)
|
.put(DateTime.now(UTC).plusWeeks(8), ACTIVE)
|
||||||
@@ -119,8 +126,8 @@ public class FeatureFlagTest extends EntityTestCase {
|
|||||||
.build());
|
.build());
|
||||||
persistResource(
|
persistResource(
|
||||||
new FeatureFlag.Builder()
|
new FeatureFlag.Builder()
|
||||||
.setFeatureName("testFlag2")
|
.setFeatureName(MINIMUM_DATASET_CONTACTS_OPTIONAL)
|
||||||
.setStatus(
|
.setStatusMap(
|
||||||
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
|
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
|
||||||
.put(START_OF_TIME, INACTIVE)
|
.put(START_OF_TIME, INACTIVE)
|
||||||
.put(DateTime.now(UTC).plusWeeks(3), INACTIVE)
|
.put(DateTime.now(UTC).plusWeeks(3), INACTIVE)
|
||||||
@@ -129,74 +136,48 @@ public class FeatureFlagTest extends EntityTestCase {
|
|||||||
FeatureFlagNotFoundException thrown =
|
FeatureFlagNotFoundException thrown =
|
||||||
assertThrows(
|
assertThrows(
|
||||||
FeatureFlagNotFoundException.class,
|
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)
|
assertThat(thrown)
|
||||||
.hasMessageThat()
|
.hasMessageThat()
|
||||||
.isEqualTo("No feature flag object(s) found for missingFlag");
|
.isEqualTo("No feature flag object(s) found for MINIMUM_DATASET_CONTACTS_PROHIBITED");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testFailure_featureFlagNotPresent() {
|
void testFailure_featureFlagNotPresent() {
|
||||||
FeatureFlagNotFoundException thrown =
|
FeatureFlagNotFoundException thrown =
|
||||||
assertThrows(FeatureFlagNotFoundException.class, () -> FeatureFlag.get("fakeFlag"));
|
assertThrows(FeatureFlagNotFoundException.class, () -> FeatureFlag.get(TEST_FEATURE));
|
||||||
assertThat(thrown).hasMessageThat().isEqualTo("No feature flag object(s) found for fakeFlag");
|
assertThat(thrown)
|
||||||
}
|
.hasMessageThat()
|
||||||
|
.isEqualTo("No feature flag object(s) found for TEST_FEATURE");
|
||||||
@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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testFailure_nullFeatureName() {
|
void testFailure_nullFeatureName() {
|
||||||
FeatureFlag.Builder featureFlagBuilder =
|
FeatureFlag.Builder featureFlagBuilder =
|
||||||
new FeatureFlag.Builder()
|
new FeatureFlag.Builder()
|
||||||
.setStatus(
|
.setStatusMap(
|
||||||
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
|
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
|
||||||
.put(START_OF_TIME, INACTIVE)
|
.put(START_OF_TIME, INACTIVE)
|
||||||
.put(DateTime.now(UTC).plusWeeks(8), ACTIVE)
|
.put(DateTime.now(UTC).plusWeeks(8), ACTIVE)
|
||||||
.build());
|
.build());
|
||||||
IllegalArgumentException thrown =
|
IllegalArgumentException thrown =
|
||||||
assertThrows(IllegalArgumentException.class, () -> featureFlagBuilder.build());
|
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_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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testFailure_invalidStatusMap() {
|
void testFailure_invalidStatusMap() {
|
||||||
FeatureFlag.Builder featureFlagBuilder = new FeatureFlag.Builder().setFeatureName("testFlag");
|
FeatureFlag.Builder featureFlagBuilder = new FeatureFlag.Builder().setFeatureName(TEST_FEATURE);
|
||||||
IllegalArgumentException thrown =
|
IllegalArgumentException thrown =
|
||||||
assertThrows(
|
assertThrows(
|
||||||
IllegalArgumentException.class,
|
IllegalArgumentException.class,
|
||||||
() ->
|
() ->
|
||||||
featureFlagBuilder.setStatus(
|
featureFlagBuilder.setStatusMap(
|
||||||
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
|
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
|
||||||
.put(DateTime.now(UTC).plusWeeks(8), ACTIVE)
|
.put(DateTime.now(UTC).plusWeeks(8), ACTIVE)
|
||||||
.build()));
|
.build()));
|
||||||
|
|||||||
@@ -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)");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
featureName: "TEST_FEATURE"
|
||||||
|
status:
|
||||||
|
"1970-01-01T00:00:00.000Z": "INACTIVE"
|
||||||
|
"1985-02-15T06:07:08.789Z": "ACTIVE"
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
featureName: "TEST_FEATURE"
|
||||||
|
status:
|
||||||
|
"1970-01-01T00:00:00.000Z": "INACTIVE"
|
||||||
|
"1985-02-15T06:07:08.789Z": "ACTIVE"
|
||||||
|
|
||||||
|
featureName: "MINIMUM_DATASET_CONTACTS_OPTIONAL"
|
||||||
|
status:
|
||||||
|
"1970-01-01T00:00:00.000Z": "INACTIVE"
|
||||||
|
"1984-12-28T06:07:08.789Z": "ACTIVE"
|
||||||
|
"1985-02-15T06:07:08.789Z": "INACTIVE"
|
||||||
|
"1985-03-01T06:07:08.789Z": "ACTIVE"
|
||||||
|
|
||||||
|
featureName: "MINIMUM_DATASET_CONTACTS_PROHIBITED"
|
||||||
|
status:
|
||||||
|
"1970-01-01T00:00:00.000Z": "ACTIVE"
|
||||||
Reference in New Issue
Block a user