mirror of
https://github.com/google/nomulus
synced 2026-01-07 05:56:49 +00:00
Sometimes include deletion times in domain-list exports (#2602)
We only include the deletion time if the domain is in the 5-day PENDING_DELETE period after the 30 day REDEMPTION period. For all other domains, we just have an empty string as that field. This is behind a feature flag so that we can control when it is enabled
This commit is contained in:
@@ -17,6 +17,7 @@ package google.registry.export;
|
|||||||
import static com.google.common.base.Verify.verifyNotNull;
|
import static com.google.common.base.Verify.verifyNotNull;
|
||||||
import static google.registry.model.tld.Tlds.getTldsOfType;
|
import static google.registry.model.tld.Tlds.getTldsOfType;
|
||||||
import static google.registry.persistence.PersistenceModule.TransactionIsolationLevel.TRANSACTION_REPEATABLE_READ;
|
import static google.registry.persistence.PersistenceModule.TransactionIsolationLevel.TRANSACTION_REPEATABLE_READ;
|
||||||
|
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
|
||||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||||
import static google.registry.request.Action.Method.POST;
|
import static google.registry.request.Action.Method.POST;
|
||||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
@@ -28,6 +29,9 @@ import com.google.common.flogger.FluentLogger;
|
|||||||
import com.google.common.net.MediaType;
|
import com.google.common.net.MediaType;
|
||||||
import google.registry.config.RegistryConfig.Config;
|
import google.registry.config.RegistryConfig.Config;
|
||||||
import google.registry.gcs.GcsUtils;
|
import google.registry.gcs.GcsUtils;
|
||||||
|
import google.registry.model.common.FeatureFlag;
|
||||||
|
import google.registry.model.domain.rgp.GracePeriodStatus;
|
||||||
|
import google.registry.model.eppcommon.StatusValue;
|
||||||
import google.registry.model.tld.Tld;
|
import google.registry.model.tld.Tld;
|
||||||
import google.registry.model.tld.Tld.TldType;
|
import google.registry.model.tld.Tld.TldType;
|
||||||
import google.registry.request.Action;
|
import google.registry.request.Action;
|
||||||
@@ -38,8 +42,13 @@ import google.registry.util.Clock;
|
|||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.OutputStreamWriter;
|
import java.io.OutputStreamWriter;
|
||||||
import java.io.Writer;
|
import java.io.Writer;
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
import org.hibernate.query.NativeQuery;
|
||||||
|
import org.hibernate.query.TupleTransformer;
|
||||||
|
import org.joda.time.DateTime;
|
||||||
|
import org.joda.time.DateTimeZone;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An action that exports the list of active domains on all real TLDs to Google Drive and GCS.
|
* An action that exports the list of active domains on all real TLDs to Google Drive and GCS.
|
||||||
@@ -55,7 +64,21 @@ import javax.inject.Inject;
|
|||||||
public class ExportDomainListsAction implements Runnable {
|
public class ExportDomainListsAction implements Runnable {
|
||||||
|
|
||||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||||
public static final String REGISTERED_DOMAINS_FILENAME = "registered_domains.txt";
|
private static final String SELECT_DOMAINS_STATEMENT =
|
||||||
|
"SELECT domainName FROM Domain WHERE tld = :tld AND deletionTime > :now ORDER by domainName";
|
||||||
|
private static final String SELECT_DOMAINS_AND_DELETION_TIMES_STATEMENT =
|
||||||
|
"""
|
||||||
|
SELECT d.domain_name, d.deletion_time, d.statuses, gp.type FROM "Domain" d
|
||||||
|
LEFT JOIN (SELECT type, domain_repo_id FROM "GracePeriod"
|
||||||
|
WHERE type = 'REDEMPTION'
|
||||||
|
AND expiration_time > CAST(:now AS timestamptz)) AS gp
|
||||||
|
ON d.repo_id = gp.domain_repo_id
|
||||||
|
WHERE d.tld = :tld
|
||||||
|
AND d.deletion_time > CAST(:now AS timestamptz)
|
||||||
|
ORDER BY d.domain_name""";
|
||||||
|
|
||||||
|
// This may be a CSV, but it is uses a .txt file extension for back-compatibility
|
||||||
|
static final String REGISTERED_DOMAINS_FILENAME = "registered_domains.txt";
|
||||||
|
|
||||||
@Inject Clock clock;
|
@Inject Clock clock;
|
||||||
@Inject DriveConnection driveConnection;
|
@Inject DriveConnection driveConnection;
|
||||||
@@ -68,47 +91,50 @@ public class ExportDomainListsAction implements Runnable {
|
|||||||
public void run() {
|
public void run() {
|
||||||
ImmutableSet<String> realTlds = getTldsOfType(TldType.REAL);
|
ImmutableSet<String> realTlds = getTldsOfType(TldType.REAL);
|
||||||
logger.atInfo().log("Exporting domain lists for TLDs %s.", realTlds);
|
logger.atInfo().log("Exporting domain lists for TLDs %s.", realTlds);
|
||||||
|
|
||||||
|
boolean includeDeletionTimes =
|
||||||
|
tm().transact(
|
||||||
|
() ->
|
||||||
|
FeatureFlag.isActiveNowOrElse(
|
||||||
|
FeatureFlag.FeatureName.INCLUDE_PENDING_DELETE_DATE_FOR_DOMAINS, false));
|
||||||
realTlds.forEach(
|
realTlds.forEach(
|
||||||
tld -> {
|
tld -> {
|
||||||
List<String> domains =
|
List<String> domainsList =
|
||||||
tm().transact(
|
replicaTm()
|
||||||
|
.transact(
|
||||||
TRANSACTION_REPEATABLE_READ,
|
TRANSACTION_REPEATABLE_READ,
|
||||||
() ->
|
() -> {
|
||||||
// Note that if we had "creationTime <= :now" in the condition (not
|
if (includeDeletionTimes) {
|
||||||
// necessary as there is no pending creation, the order of deletionTime
|
// We want to include deletion times, but only for domains in the 5-day
|
||||||
// and creationTime in the query would have been significant and it
|
// PENDING_DELETE period after the REDEMPTION grace period. In order to
|
||||||
// should come after deletionTime. When Hibernate substitutes "now" it
|
// accomplish this without loading the entire list of domains, we use a
|
||||||
// will first validate that the **first** field that is to be compared
|
// native query to join against the GracePeriod table to find
|
||||||
// with it (deletionTime) is assignable from the substituted Java object
|
// PENDING_DELETE domains that don't have a REDEMPTION grace period.
|
||||||
// (click.nowUtc()). Since creationTime is a CreateAutoTimestamp, if it
|
return replicaTm()
|
||||||
// comes first, we will need to substitute "now" with
|
.getEntityManager()
|
||||||
// CreateAutoTimestamp.create(clock.nowUtc()). This might look a bit
|
.createNativeQuery(SELECT_DOMAINS_AND_DELETION_TIMES_STATEMENT)
|
||||||
// strange as the Java object type is clearly incompatible between the
|
.unwrap(NativeQuery.class)
|
||||||
// two fields deletionTime (DateTime) and creationTime, yet they are
|
.setTupleTransformer(new DomainResultTransformer())
|
||||||
// compared with the same "now". It is actually OK because in the end
|
|
||||||
// Hibernate converts everything to SQL types (and Java field names to
|
|
||||||
// SQL column names) to run the query. Both CreateAutoTimestamp and
|
|
||||||
// DateTime are persisted as timestamp_z in SQL. It is only the
|
|
||||||
// validation that compares the Java types, and only with the first
|
|
||||||
// field that compares with the substituted value.
|
|
||||||
tm().query(
|
|
||||||
"SELECT domainName FROM Domain "
|
|
||||||
+ "WHERE tld = :tld "
|
|
||||||
+ "AND deletionTime > :now "
|
|
||||||
+ "ORDER by domainName ASC",
|
|
||||||
String.class)
|
|
||||||
.setParameter("tld", tld)
|
.setParameter("tld", tld)
|
||||||
.setParameter("now", clock.nowUtc())
|
.setParameter("now", replicaTm().getTransactionTime().toString())
|
||||||
.getResultList());
|
.getResultList();
|
||||||
String domainsList = Joiner.on("\n").join(domains);
|
} else {
|
||||||
|
return replicaTm()
|
||||||
|
.query(SELECT_DOMAINS_STATEMENT, String.class)
|
||||||
|
.setParameter("tld", tld)
|
||||||
|
.setParameter("now", replicaTm().getTransactionTime())
|
||||||
|
.getResultList();
|
||||||
|
}
|
||||||
|
});
|
||||||
logger.atInfo().log(
|
logger.atInfo().log(
|
||||||
"Exporting %d domains for TLD %s to GCS and Drive.", domains.size(), tld);
|
"Exporting %d domains for TLD %s to GCS and Drive.", domainsList.size(), tld);
|
||||||
exportToGcs(tld, domainsList, gcsBucket, gcsUtils);
|
String domainsListOutput = Joiner.on('\n').join(domainsList);
|
||||||
exportToDrive(tld, domainsList, driveConnection);
|
exportToGcs(tld, domainsListOutput, gcsBucket, gcsUtils);
|
||||||
|
exportToDrive(tld, domainsListOutput, driveConnection);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static boolean exportToDrive(
|
protected static void exportToDrive(
|
||||||
String tldStr, String domains, DriveConnection driveConnection) {
|
String tldStr, String domains, DriveConnection driveConnection) {
|
||||||
verifyNotNull(driveConnection, "Expecting non-null driveConnection");
|
verifyNotNull(driveConnection, "Expecting non-null driveConnection");
|
||||||
try {
|
try {
|
||||||
@@ -131,12 +157,10 @@ public class ExportDomainListsAction implements Runnable {
|
|||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
logger.atSevere().withCause(e).log(
|
logger.atSevere().withCause(e).log(
|
||||||
"Error exporting registered domains for TLD %s to Drive, skipping...", tldStr);
|
"Error exporting registered domains for TLD %s to Drive, skipping...", tldStr);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static boolean exportToGcs(
|
protected static void exportToGcs(
|
||||||
String tld, String domains, String gcsBucket, GcsUtils gcsUtils) {
|
String tld, String domains, String gcsBucket, GcsUtils gcsUtils) {
|
||||||
BlobId blobId = BlobId.of(gcsBucket, tld + ".txt");
|
BlobId blobId = BlobId.of(gcsBucket, tld + ".txt");
|
||||||
try (OutputStream gcsOutput = gcsUtils.openOutputStream(blobId);
|
try (OutputStream gcsOutput = gcsUtils.openOutputStream(blobId);
|
||||||
@@ -145,8 +169,22 @@ public class ExportDomainListsAction implements Runnable {
|
|||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
logger.atSevere().withCause(e).log(
|
logger.atSevere().withCause(e).log(
|
||||||
"Error exporting registered domains for TLD %s to GCS, skipping...", tld);
|
"Error exporting registered domains for TLD %s to GCS, skipping...", tld);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
return true;
|
}
|
||||||
|
|
||||||
|
/** Transforms the multiple columns selected from SQL into the output line. */
|
||||||
|
private static class DomainResultTransformer implements TupleTransformer<String> {
|
||||||
|
@Override
|
||||||
|
public String transformTuple(Object[] domainResult, String[] strings) {
|
||||||
|
String domainName = (String) domainResult[0];
|
||||||
|
Instant deletionInstant = (Instant) domainResult[1];
|
||||||
|
DateTime deletionTime = new DateTime(deletionInstant.toEpochMilli(), DateTimeZone.UTC);
|
||||||
|
String[] domainStatuses = (String[]) domainResult[2];
|
||||||
|
String gracePeriodType = (String) domainResult[3];
|
||||||
|
boolean inPendingDelete =
|
||||||
|
ImmutableSet.copyOf(domainStatuses).contains(StatusValue.PENDING_DELETE.toString())
|
||||||
|
&& !GracePeriodStatus.REDEMPTION.toString().equals(gracePeriodType);
|
||||||
|
return String.format("%s,%s", domainName, inPendingDelete ? deletionTime : "");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ public class FeatureFlag extends ImmutableObject implements Buildable {
|
|||||||
TEST_FEATURE,
|
TEST_FEATURE,
|
||||||
MINIMUM_DATASET_CONTACTS_OPTIONAL,
|
MINIMUM_DATASET_CONTACTS_OPTIONAL,
|
||||||
MINIMUM_DATASET_CONTACTS_PROHIBITED,
|
MINIMUM_DATASET_CONTACTS_PROHIBITED,
|
||||||
|
INCLUDE_PENDING_DELETE_DATE_FOR_DOMAINS
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The name of the flag/feature. */
|
/** The name of the flag/feature. */
|
||||||
@@ -154,6 +155,15 @@ public class FeatureFlag extends ImmutableObject implements Buildable {
|
|||||||
return status.getValueAtTime(time);
|
return status.getValueAtTime(time);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns if the flag is active, or the default value if the flag does not exist. */
|
||||||
|
public static boolean isActiveNowOrElse(FeatureName featureName, boolean defaultValue) {
|
||||||
|
tm().assertInTransaction();
|
||||||
|
return CACHE
|
||||||
|
.get(featureName)
|
||||||
|
.map(flag -> flag.getStatus(tm().getTransactionTime()).equals(ACTIVE))
|
||||||
|
.orElse(defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns if the FeatureFlag with the given FeatureName is active now. */
|
/** Returns if the FeatureFlag with the given FeatureName is active now. */
|
||||||
public static boolean isActiveNow(FeatureName featureName) {
|
public static boolean isActiveNow(FeatureName featureName) {
|
||||||
tm().assertInTransaction();
|
tm().assertInTransaction();
|
||||||
|
|||||||
@@ -16,10 +16,14 @@ package google.registry.export;
|
|||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
import static google.registry.export.ExportDomainListsAction.REGISTERED_DOMAINS_FILENAME;
|
import static google.registry.export.ExportDomainListsAction.REGISTERED_DOMAINS_FILENAME;
|
||||||
|
import static google.registry.model.common.FeatureFlag.FeatureName.INCLUDE_PENDING_DELETE_DATE_FOR_DOMAINS;
|
||||||
|
import static google.registry.model.common.FeatureFlag.FeatureStatus.ACTIVE;
|
||||||
|
import static google.registry.model.common.FeatureFlag.FeatureStatus.INACTIVE;
|
||||||
import static google.registry.testing.DatabaseHelper.createTld;
|
import static google.registry.testing.DatabaseHelper.createTld;
|
||||||
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
|
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
|
||||||
import static google.registry.testing.DatabaseHelper.persistDeletedDomain;
|
import static google.registry.testing.DatabaseHelper.persistDeletedDomain;
|
||||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||||
|
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
@@ -31,8 +35,14 @@ import com.google.cloud.storage.BlobId;
|
|||||||
import com.google.cloud.storage.StorageException;
|
import com.google.cloud.storage.StorageException;
|
||||||
import com.google.cloud.storage.contrib.nio.testing.LocalStorageHelper;
|
import com.google.cloud.storage.contrib.nio.testing.LocalStorageHelper;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.collect.ImmutableSortedMap;
|
||||||
import com.google.common.net.MediaType;
|
import com.google.common.net.MediaType;
|
||||||
import google.registry.gcs.GcsUtils;
|
import google.registry.gcs.GcsUtils;
|
||||||
|
import google.registry.model.common.FeatureFlag;
|
||||||
|
import google.registry.model.domain.Domain;
|
||||||
|
import google.registry.model.domain.GracePeriod;
|
||||||
|
import google.registry.model.domain.rgp.GracePeriodStatus;
|
||||||
|
import google.registry.model.eppcommon.StatusValue;
|
||||||
import google.registry.model.tld.Tld;
|
import google.registry.model.tld.Tld;
|
||||||
import google.registry.model.tld.Tld.TldType;
|
import google.registry.model.tld.Tld.TldType;
|
||||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||||
@@ -56,7 +66,7 @@ class ExportDomainListsActionTest {
|
|||||||
|
|
||||||
@RegisterExtension
|
@RegisterExtension
|
||||||
final JpaIntegrationTestExtension jpa =
|
final JpaIntegrationTestExtension jpa =
|
||||||
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
|
new JpaTestExtensions.Builder().withClock(clock).buildIntegrationTestExtension();
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void beforeEach() {
|
void beforeEach() {
|
||||||
@@ -70,6 +80,7 @@ class ExportDomainListsActionTest {
|
|||||||
action.gcsUtils = gcsUtils;
|
action.gcsUtils = gcsUtils;
|
||||||
action.clock = clock;
|
action.clock = clock;
|
||||||
action.driveConnection = driveConnection;
|
action.driveConnection = driveConnection;
|
||||||
|
persistFeatureFlag(INACTIVE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void verifyExportedToDrive(String folderId, String domains) throws Exception {
|
private void verifyExportedToDrive(String folderId, String domains) throws Exception {
|
||||||
@@ -83,7 +94,7 @@ class ExportDomainListsActionTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void test_outputsOnlyActiveDomains() throws Exception {
|
void test_outputsOnlyActiveDomains_txt() throws Exception {
|
||||||
persistActiveDomain("onetwo.tld");
|
persistActiveDomain("onetwo.tld");
|
||||||
persistActiveDomain("rudnitzky.tld");
|
persistActiveDomain("rudnitzky.tld");
|
||||||
persistDeletedDomain("mortuary.tld", DateTime.parse("2001-03-14T10:11:12Z"));
|
persistDeletedDomain("mortuary.tld", DateTime.parse("2001-03-14T10:11:12Z"));
|
||||||
@@ -97,7 +108,22 @@ class ExportDomainListsActionTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void test_outputsOnlyDomainsOnRealTlds() throws Exception {
|
void test_outputsOnlyActiveDomains_csv() throws Exception {
|
||||||
|
persistFeatureFlag(ACTIVE);
|
||||||
|
persistActiveDomain("onetwo.tld");
|
||||||
|
persistActiveDomain("rudnitzky.tld");
|
||||||
|
persistDeletedDomain("mortuary.tld", DateTime.parse("2001-03-14T10:11:12Z"));
|
||||||
|
action.run();
|
||||||
|
BlobId existingFile = BlobId.of("outputbucket", "tld.txt");
|
||||||
|
String tlds = new String(gcsUtils.readBytesFrom(existingFile), UTF_8);
|
||||||
|
// Check that it only contains the active domains, not the dead one.
|
||||||
|
assertThat(tlds).isEqualTo("onetwo.tld,\nrudnitzky.tld,");
|
||||||
|
verifyExportedToDrive("brouhaha", "onetwo.tld,\nrudnitzky.tld,");
|
||||||
|
verifyNoMoreInteractions(driveConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_outputsOnlyDomainsOnRealTlds_txt() throws Exception {
|
||||||
persistActiveDomain("onetwo.tld");
|
persistActiveDomain("onetwo.tld");
|
||||||
persistActiveDomain("rudnitzky.tld");
|
persistActiveDomain("rudnitzky.tld");
|
||||||
persistActiveDomain("wontgo.testtld");
|
persistActiveDomain("wontgo.testtld");
|
||||||
@@ -116,7 +142,58 @@ class ExportDomainListsActionTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void test_outputsDomainsFromDifferentTldsToMultipleFiles() throws Exception {
|
void test_outputsOnlyDomainsOnRealTlds_csv() throws Exception {
|
||||||
|
persistFeatureFlag(ACTIVE);
|
||||||
|
persistActiveDomain("onetwo.tld");
|
||||||
|
persistActiveDomain("rudnitzky.tld");
|
||||||
|
persistActiveDomain("wontgo.testtld");
|
||||||
|
action.run();
|
||||||
|
BlobId existingFile = BlobId.of("outputbucket", "tld.txt");
|
||||||
|
String tlds = new String(gcsUtils.readBytesFrom(existingFile), UTF_8).trim();
|
||||||
|
// Check that it only contains the domains on the real TLD, and not the test one.
|
||||||
|
assertThat(tlds).isEqualTo("onetwo.tld,\nrudnitzky.tld,");
|
||||||
|
// Make sure that the test TLD file wasn't written out.
|
||||||
|
BlobId nonexistentFile = BlobId.of("outputbucket", "testtld.txt");
|
||||||
|
assertThrows(StorageException.class, () -> gcsUtils.readBytesFrom(nonexistentFile));
|
||||||
|
ImmutableList<String> ls = gcsUtils.listFolderObjects("outputbucket", "");
|
||||||
|
assertThat(ls).containsExactly("tld.txt");
|
||||||
|
verifyExportedToDrive("brouhaha", "onetwo.tld,\nrudnitzky.tld,");
|
||||||
|
verifyNoMoreInteractions(driveConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_outputIncludesDeletionTimes_forPendingDeletes_notRdemption() throws Exception {
|
||||||
|
persistFeatureFlag(ACTIVE);
|
||||||
|
// Domains pending delete (meaning the 5 day period, not counting the 30 day redemption period)
|
||||||
|
// should include their pending deletion date
|
||||||
|
persistActiveDomain("active.tld");
|
||||||
|
Domain redemption = persistActiveDomain("redemption.tld");
|
||||||
|
persistResource(
|
||||||
|
redemption
|
||||||
|
.asBuilder()
|
||||||
|
.addStatusValue(StatusValue.PENDING_DELETE)
|
||||||
|
.addGracePeriod(
|
||||||
|
GracePeriod.createWithoutBillingEvent(
|
||||||
|
GracePeriodStatus.REDEMPTION,
|
||||||
|
redemption.getRepoId(),
|
||||||
|
clock.nowUtc().plusDays(20),
|
||||||
|
redemption.getCurrentSponsorRegistrarId()))
|
||||||
|
.build());
|
||||||
|
persistResource(
|
||||||
|
persistActiveDomain("pendingdelete.tld")
|
||||||
|
.asBuilder()
|
||||||
|
.addStatusValue(StatusValue.PENDING_DELETE)
|
||||||
|
.setDeletionTime(clock.nowUtc().plusDays(3))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
action.run();
|
||||||
|
|
||||||
|
verifyExportedToDrive(
|
||||||
|
"brouhaha", "active.tld,\npendingdelete.tld,2020-02-05T02:02:02.000Z\nredemption.tld,");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_outputsDomainsFromDifferentTldsToMultipleFiles_txt() throws Exception {
|
||||||
createTld("tldtwo");
|
createTld("tldtwo");
|
||||||
persistResource(Tld.get("tldtwo").asBuilder().setDriveFolderId("hooray").build());
|
persistResource(Tld.get("tldtwo").asBuilder().setDriveFolderId("hooray").build());
|
||||||
|
|
||||||
@@ -143,4 +220,43 @@ class ExportDomainListsActionTest {
|
|||||||
// tldthree does not have a drive id, so no export to drive is performed.
|
// tldthree does not have a drive id, so no export to drive is performed.
|
||||||
verifyNoMoreInteractions(driveConnection);
|
verifyNoMoreInteractions(driveConnection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_outputsDomainsFromDifferentTldsToMultipleFiles_csv() throws Exception {
|
||||||
|
persistFeatureFlag(ACTIVE);
|
||||||
|
createTld("tldtwo");
|
||||||
|
persistResource(Tld.get("tldtwo").asBuilder().setDriveFolderId("hooray").build());
|
||||||
|
|
||||||
|
createTld("tldthree");
|
||||||
|
// You'd think this test was written around Christmas, but it wasn't.
|
||||||
|
persistActiveDomain("dasher.tld");
|
||||||
|
persistActiveDomain("prancer.tld");
|
||||||
|
persistActiveDomain("rudolph.tldtwo");
|
||||||
|
persistActiveDomain("santa.tldtwo");
|
||||||
|
persistActiveDomain("buddy.tldtwo");
|
||||||
|
persistActiveDomain("cupid.tldthree");
|
||||||
|
action.run();
|
||||||
|
BlobId firstTldFile = BlobId.of("outputbucket", "tld.txt");
|
||||||
|
String tlds = new String(gcsUtils.readBytesFrom(firstTldFile), UTF_8).trim();
|
||||||
|
assertThat(tlds).isEqualTo("dasher.tld,\nprancer.tld,");
|
||||||
|
BlobId secondTldFile = BlobId.of("outputbucket", "tldtwo.txt");
|
||||||
|
String moreTlds = new String(gcsUtils.readBytesFrom(secondTldFile), UTF_8).trim();
|
||||||
|
assertThat(moreTlds).isEqualTo("buddy.tldtwo,\nrudolph.tldtwo,\nsanta.tldtwo,");
|
||||||
|
BlobId thirdTldFile = BlobId.of("outputbucket", "tldthree.txt");
|
||||||
|
String evenMoreTlds = new String(gcsUtils.readBytesFrom(thirdTldFile), UTF_8).trim();
|
||||||
|
assertThat(evenMoreTlds).isEqualTo("cupid.tldthree,");
|
||||||
|
verifyExportedToDrive("brouhaha", "dasher.tld,\nprancer.tld,");
|
||||||
|
verifyExportedToDrive("hooray", "buddy.tldtwo,\nrudolph.tldtwo,\nsanta.tldtwo,");
|
||||||
|
// tldthree does not have a drive id, so no export to drive is performed.
|
||||||
|
verifyNoMoreInteractions(driveConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void persistFeatureFlag(FeatureFlag.FeatureStatus status) {
|
||||||
|
persistResource(
|
||||||
|
new FeatureFlag()
|
||||||
|
.asBuilder()
|
||||||
|
.setFeatureName(INCLUDE_PENDING_DELETE_DATE_FOR_DOMAINS)
|
||||||
|
.setStatusMap(ImmutableSortedMap.of(START_OF_TIME, status))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
|
|||||||
import static google.registry.testing.DatabaseHelper.loadByEntity;
|
import static google.registry.testing.DatabaseHelper.loadByEntity;
|
||||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||||
import static org.joda.time.DateTimeZone.UTC;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
@@ -33,6 +32,7 @@ import google.registry.model.EntityTestCase;
|
|||||||
import google.registry.model.common.FeatureFlag.FeatureFlagNotFoundException;
|
import google.registry.model.common.FeatureFlag.FeatureFlagNotFoundException;
|
||||||
import google.registry.model.common.FeatureFlag.FeatureStatus;
|
import google.registry.model.common.FeatureFlag.FeatureStatus;
|
||||||
import org.joda.time.DateTime;
|
import org.joda.time.DateTime;
|
||||||
|
import org.joda.time.Duration;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
/** Unit tests for {@link FeatureFlag}. */
|
/** Unit tests for {@link FeatureFlag}. */
|
||||||
@@ -50,7 +50,7 @@ public class FeatureFlagTest extends EntityTestCase {
|
|||||||
.setStatusMap(
|
.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(fakeClock.nowUtc().plusWeeks(8), ACTIVE)
|
||||||
.build())
|
.build())
|
||||||
.build();
|
.build();
|
||||||
persistResource(featureFlag);
|
persistResource(featureFlag);
|
||||||
@@ -66,7 +66,7 @@ public class FeatureFlagTest extends EntityTestCase {
|
|||||||
.setStatusMap(
|
.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(fakeClock.nowUtc().plusWeeks(8), ACTIVE)
|
||||||
.build())
|
.build())
|
||||||
.build();
|
.build();
|
||||||
persistResource(featureFlag);
|
persistResource(featureFlag);
|
||||||
@@ -82,7 +82,7 @@ public class FeatureFlagTest extends EntityTestCase {
|
|||||||
.setStatusMap(
|
.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(fakeClock.nowUtc().plusWeeks(8), ACTIVE)
|
||||||
.build())
|
.build())
|
||||||
.build());
|
.build());
|
||||||
FeatureFlag featureFlag2 =
|
FeatureFlag featureFlag2 =
|
||||||
@@ -92,7 +92,7 @@ public class FeatureFlagTest extends EntityTestCase {
|
|||||||
.setStatusMap(
|
.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(fakeClock.nowUtc().plusWeeks(3), INACTIVE)
|
||||||
.build())
|
.build())
|
||||||
.build());
|
.build());
|
||||||
FeatureFlag featureFlag3 =
|
FeatureFlag featureFlag3 =
|
||||||
@@ -122,7 +122,7 @@ public class FeatureFlagTest extends EntityTestCase {
|
|||||||
.setStatusMap(
|
.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(fakeClock.nowUtc().plusWeeks(8), ACTIVE)
|
||||||
.build())
|
.build())
|
||||||
.build());
|
.build());
|
||||||
persistResource(
|
persistResource(
|
||||||
@@ -131,7 +131,7 @@ public class FeatureFlagTest extends EntityTestCase {
|
|||||||
.setStatusMap(
|
.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(fakeClock.nowUtc().plusWeeks(3), INACTIVE)
|
||||||
.build())
|
.build())
|
||||||
.build());
|
.build());
|
||||||
FeatureFlagNotFoundException thrown =
|
FeatureFlagNotFoundException thrown =
|
||||||
@@ -164,7 +164,7 @@ public class FeatureFlagTest extends EntityTestCase {
|
|||||||
.setStatusMap(
|
.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(fakeClock.nowUtc().plusWeeks(8), ACTIVE)
|
||||||
.build());
|
.build());
|
||||||
IllegalArgumentException thrown =
|
IllegalArgumentException thrown =
|
||||||
assertThrows(IllegalArgumentException.class, () -> featureFlagBuilder.build());
|
assertThrows(IllegalArgumentException.class, () -> featureFlagBuilder.build());
|
||||||
@@ -180,7 +180,7 @@ public class FeatureFlagTest extends EntityTestCase {
|
|||||||
() ->
|
() ->
|
||||||
featureFlagBuilder.setStatusMap(
|
featureFlagBuilder.setStatusMap(
|
||||||
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
|
ImmutableSortedMap.<DateTime, FeatureStatus>naturalOrder()
|
||||||
.put(DateTime.now(UTC).plusWeeks(8), ACTIVE)
|
.put(fakeClock.nowUtc().plusWeeks(8), ACTIVE)
|
||||||
.build()));
|
.build()));
|
||||||
assertThat(thrown)
|
assertThat(thrown)
|
||||||
.hasMessageThat()
|
.hasMessageThat()
|
||||||
@@ -203,4 +203,37 @@ public class FeatureFlagTest extends EntityTestCase {
|
|||||||
fakeClock.setTo(DateTime.parse("2011-10-17TZ"));
|
fakeClock.setTo(DateTime.parse("2011-10-17TZ"));
|
||||||
assertThat(tm().transact(() -> FeatureFlag.isActiveNow(TEST_FEATURE))).isTrue();
|
assertThat(tm().transact(() -> FeatureFlag.isActiveNow(TEST_FEATURE))).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSuccess_default_exists() {
|
||||||
|
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());
|
||||||
|
tm().transact(
|
||||||
|
() -> {
|
||||||
|
assertThat(FeatureFlag.isActiveNowOrElse(TEST_FEATURE, false)).isFalse();
|
||||||
|
assertThat(FeatureFlag.isActiveNowOrElse(TEST_FEATURE, true)).isFalse();
|
||||||
|
});
|
||||||
|
fakeClock.advanceBy(Duration.standardDays(365));
|
||||||
|
tm().transact(
|
||||||
|
() -> {
|
||||||
|
assertThat(FeatureFlag.isActiveNowOrElse(TEST_FEATURE, false)).isTrue();
|
||||||
|
assertThat(FeatureFlag.isActiveNowOrElse(TEST_FEATURE, true)).isTrue();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSuccess_default_doesNotExist() {
|
||||||
|
tm().transact(
|
||||||
|
() -> {
|
||||||
|
assertThat(FeatureFlag.isActiveNowOrElse(TEST_FEATURE, false)).isFalse();
|
||||||
|
assertThat(FeatureFlag.isActiveNowOrElse(TEST_FEATURE, true)).isTrue();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -470,7 +470,7 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
create table "FeatureFlag" (
|
create table "FeatureFlag" (
|
||||||
feature_name text not null check (feature_name in ('TEST_FEATURE','MINIMUM_DATASET_CONTACTS_OPTIONAL','MINIMUM_DATASET_CONTACTS_PROHIBITED')),
|
feature_name text not null check (feature_name in ('TEST_FEATURE','MINIMUM_DATASET_CONTACTS_OPTIONAL','MINIMUM_DATASET_CONTACTS_PROHIBITED','INCLUDE_PENDING_DELETE_DATE_FOR_DOMAINS')),
|
||||||
status hstore not null,
|
status hstore not null,
|
||||||
primary key (feature_name)
|
primary key (feature_name)
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user