diff --git a/core/src/main/java/google/registry/model/registry/RegistryLockDao.java b/core/src/main/java/google/registry/model/registry/RegistryLockDao.java index 7cf61f3e8..003be5cbc 100644 --- a/core/src/main/java/google/registry/model/registry/RegistryLockDao.java +++ b/core/src/main/java/google/registry/model/registry/RegistryLockDao.java @@ -52,8 +52,10 @@ public final class RegistryLockDao { jpaTm() .getEntityManager() .createQuery( - "SELECT lock FROM RegistryLock lock WHERE lock.registrarId = :registrarId" - + " AND lock.unlockCompletionTimestamp IS NULL", + "SELECT lock FROM RegistryLock lock" + + " WHERE lock.registrarId = :registrarId" + + " AND lock.unlockCompletionTimestamp IS NULL" + + " ORDER BY lock.domainName ASC", RegistryLock.class) .setParameter("registrarId", registrarId) .getResultList()); diff --git a/core/src/main/java/google/registry/persistence/converter/DurationConverter.java b/core/src/main/java/google/registry/persistence/converter/DurationConverter.java new file mode 100644 index 000000000..8808bc371 --- /dev/null +++ b/core/src/main/java/google/registry/persistence/converter/DurationConverter.java @@ -0,0 +1,37 @@ +// Copyright 2020 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.persistence.converter; + +import javax.annotation.Nullable; +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; +import org.joda.time.Duration; + +/** JPA converter to for storing/retrieving {@link org.joda.time.DateTime} objects. */ +@Converter(autoApply = true) +public class DurationConverter implements AttributeConverter { + + @Override + @Nullable + public Long convertToDatabaseColumn(@Nullable Duration duration) { + return duration == null ? null : duration.getMillis(); + } + + @Override + @Nullable + public Duration convertToEntityAttribute(@Nullable Long dbData) { + return dbData == null ? null : new Duration(dbData); + } +} diff --git a/core/src/main/java/google/registry/schema/domain/RegistryLock.java b/core/src/main/java/google/registry/schema/domain/RegistryLock.java index 63e213e8e..f6b2ca4e2 100644 --- a/core/src/main/java/google/registry/schema/domain/RegistryLock.java +++ b/core/src/main/java/google/registry/schema/domain/RegistryLock.java @@ -26,6 +26,7 @@ import google.registry.model.UpdateAutoTimestamp; import google.registry.util.DateTimeUtils; import java.time.ZonedDateTime; import java.util.Optional; +import javax.annotation.Nullable; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; @@ -37,6 +38,7 @@ import javax.persistence.JoinColumn; import javax.persistence.OneToOne; import javax.persistence.Table; import org.joda.time.DateTime; +import org.joda.time.Duration; /** * Represents a registry lock/unlock object, meaning that the domain is locked on the registry @@ -131,6 +133,9 @@ public final class RegistryLock extends ImmutableObject implements Buildable { @JoinColumn(name = "relockRevisionId", referencedColumnName = "revisionId") private RegistryLock relock; + /** The duration after which we will re-lock this domain after it is unlocked. */ + private Duration relockDuration; + /** Time that this entity was last updated. */ private UpdateAutoTimestamp lastUpdateTimestamp; @@ -197,6 +202,11 @@ public final class RegistryLock extends ImmutableObject implements Buildable { return relock; } + /** The duration after which we will re-lock this domain after it is unlocked. */ + public Optional getRelockDuration() { + return Optional.ofNullable(relockDuration); + } + public boolean isLocked() { return lockCompletionTimestamp != null && unlockCompletionTimestamp == null; } @@ -289,5 +299,10 @@ public final class RegistryLock extends ImmutableObject implements Buildable { getInstance().relock = relock; return this; } + + public Builder setRelockDuration(@Nullable Duration relockDuration) { + getInstance().relockDuration = relockDuration; + return this; + } } } diff --git a/core/src/main/resources/META-INF/persistence.xml b/core/src/main/resources/META-INF/persistence.xml index 0fead4f17..392aef01c 100644 --- a/core/src/main/resources/META-INF/persistence.xml +++ b/core/src/main/resources/META-INF/persistence.xml @@ -38,6 +38,7 @@ google.registry.persistence.converter.CreateAutoTimestampConverter google.registry.persistence.converter.CurrencyUnitConverter google.registry.persistence.converter.DateTimeConverter + google.registry.persistence.converter.DurationConverter google.registry.persistence.converter.RegistrarPocSetConverter google.registry.persistence.converter.StatusValueSetConverter google.registry.persistence.converter.StringListConverter diff --git a/core/src/test/java/google/registry/model/registry/RegistryLockDaoTest.java b/core/src/test/java/google/registry/model/registry/RegistryLockDaoTest.java index ff5c5d766..c2a2209a5 100644 --- a/core/src/test/java/google/registry/model/registry/RegistryLockDaoTest.java +++ b/core/src/test/java/google/registry/model/registry/RegistryLockDaoTest.java @@ -30,6 +30,7 @@ import google.registry.schema.domain.RegistryLock; import google.registry.testing.AppEngineRule; import google.registry.testing.FakeClock; import java.util.Optional; +import org.joda.time.Duration; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -88,12 +89,14 @@ public final class RegistryLockDaoTest { .setLockCompletionTimestamp(fakeClock.nowUtc()) .setUnlockRequestTimestamp(fakeClock.nowUtc()) .setUnlockCompletionTimestamp(fakeClock.nowUtc()) + .setRelockDuration(Duration.standardHours(6)) .build()); RegistryLock fromDatabase = getRegistryLockByVerificationCode(lock.getVerificationCode()).get(); assertThat(fromDatabase.getUnlockRequestTimestamp()).isEqualTo(Optional.of(fakeClock.nowUtc())); assertThat(fromDatabase.getUnlockCompletionTimestamp()) .isEqualTo(Optional.of(fakeClock.nowUtc())); assertThat(fromDatabase.isLocked()).isFalse(); + assertThat(fromDatabase.getRelockDuration().get()).isEqualTo(Duration.standardHours(6)); } @Test diff --git a/core/src/test/java/google/registry/persistence/converter/CurrencyUnitConverterTest.java b/core/src/test/java/google/registry/persistence/converter/CurrencyUnitConverterTest.java index d21f08b04..64de67727 100644 --- a/core/src/test/java/google/registry/persistence/converter/CurrencyUnitConverterTest.java +++ b/core/src/test/java/google/registry/persistence/converter/CurrencyUnitConverterTest.java @@ -11,6 +11,7 @@ // 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.persistence.converter; import static com.google.common.truth.Truth.assertThat; diff --git a/core/src/test/java/google/registry/persistence/converter/DurationConverterTest.java b/core/src/test/java/google/registry/persistence/converter/DurationConverterTest.java new file mode 100644 index 000000000..c8e801bb2 --- /dev/null +++ b/core/src/test/java/google/registry/persistence/converter/DurationConverterTest.java @@ -0,0 +1,80 @@ +// Copyright 2020 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.persistence.converter; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm; + +import google.registry.model.ImmutableObject; +import google.registry.persistence.transaction.JpaTestRules; +import google.registry.persistence.transaction.JpaTestRules.JpaUnitTestRule; +import java.math.BigInteger; +import javax.persistence.Entity; +import javax.persistence.Id; +import org.joda.time.Duration; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link DurationConverter}. */ +@RunWith(JUnit4.class) +public class DurationConverterTest { + + @Rule + public final JpaUnitTestRule jpaRule = + new JpaTestRules.Builder().withEntityClass(TestEntity.class).buildUnitTestRule(); + + private final DurationConverter converter = new DurationConverter(); + + @Test + public void testNulls() { + assertThat(converter.convertToDatabaseColumn(null)).isNull(); + assertThat(converter.convertToEntityAttribute(null)).isNull(); + } + + @Test + public void testRoundTrip() { + TestEntity entity = new TestEntity(Duration.standardDays(6)); + jpaTm().transact(() -> jpaTm().getEntityManager().persist(entity)); + assertThat( + jpaTm() + .transact( + () -> + jpaTm() + .getEntityManager() + .createNativeQuery( + "SELECT duration FROM \"TestEntity\" WHERE name = 'id'") + .getResultList())) + .containsExactly(BigInteger.valueOf(Duration.standardDays(6).getMillis())); + TestEntity persisted = + jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "id")); + assertThat(persisted.duration).isEqualTo(Duration.standardDays(6)); + } + + @Entity(name = "TestEntity") // Override entity name to avoid the nested class reference. + public static class TestEntity extends ImmutableObject { + + @Id String name = "id"; + + Duration duration; + + public TestEntity() {} + + TestEntity(Duration duration) { + this.duration = duration; + } + } +} diff --git a/core/src/test/java/google/registry/ui/server/registrar/RegistryLockGetActionTest.java b/core/src/test/java/google/registry/ui/server/registrar/RegistryLockGetActionTest.java index b91e465a1..b177e917a 100644 --- a/core/src/test/java/google/registry/ui/server/registrar/RegistryLockGetActionTest.java +++ b/core/src/test/java/google/registry/ui/server/registrar/RegistryLockGetActionTest.java @@ -188,10 +188,10 @@ public final class RegistryLockGetActionTest { "locks", ImmutableList.of( new ImmutableMap.Builder<>() - .put("fullyQualifiedDomainName", "expiredunlock.test") - .put("lockedTime", "2000-06-08T22:00:00.000Z") - .put("lockedBy", "johndoe@theregistrar.com") - .put("userCanUnlock", true) + .put("fullyQualifiedDomainName", "adminexample.test") + .put("lockedTime", "2000-06-09T22:00:00.001Z") + .put("lockedBy", "admin") + .put("userCanUnlock", false) .put("isLockPending", false) .put("isUnlockPending", false) .build(), @@ -204,19 +204,11 @@ public final class RegistryLockGetActionTest { .put("isUnlockPending", false) .build(), new ImmutableMap.Builder<>() - .put("fullyQualifiedDomainName", "adminexample.test") - .put("lockedTime", "2000-06-09T22:00:00.001Z") - .put("lockedBy", "admin") - .put("userCanUnlock", false) - .put("isLockPending", false) - .put("isUnlockPending", false) - .build(), - new ImmutableMap.Builder<>() - .put("fullyQualifiedDomainName", "pending.test") - .put("lockedTime", "") + .put("fullyQualifiedDomainName", "expiredunlock.test") + .put("lockedTime", "2000-06-08T22:00:00.000Z") .put("lockedBy", "johndoe@theregistrar.com") .put("userCanUnlock", true) - .put("isLockPending", true) + .put("isLockPending", false) .put("isUnlockPending", false) .build(), new ImmutableMap.Builder<>() @@ -226,6 +218,14 @@ public final class RegistryLockGetActionTest { .put("userCanUnlock", true) .put("isLockPending", false) .put("isUnlockPending", true) + .build(), + new ImmutableMap.Builder<>() + .put("fullyQualifiedDomainName", "pending.test") + .put("lockedTime", "") + .put("lockedBy", "johndoe@theregistrar.com") + .put("userCanUnlock", true) + .put("isLockPending", true) + .put("isUnlockPending", false) .build())))); } diff --git a/core/src/test/resources/google/registry/webdriver/goldens/chrome-linux/RegistrarConsoleScreenshotTest_registryLock_nonEmpty_admin_page.png b/core/src/test/resources/google/registry/webdriver/goldens/chrome-linux/RegistrarConsoleScreenshotTest_registryLock_nonEmpty_admin_page.png index 6d70e570e..c79aa9ab0 100644 Binary files a/core/src/test/resources/google/registry/webdriver/goldens/chrome-linux/RegistrarConsoleScreenshotTest_registryLock_nonEmpty_admin_page.png and b/core/src/test/resources/google/registry/webdriver/goldens/chrome-linux/RegistrarConsoleScreenshotTest_registryLock_nonEmpty_admin_page.png differ diff --git a/db/src/main/resources/sql/flyway/V20__add_relock_duration.sql b/db/src/main/resources/sql/flyway/V20__add_relock_duration.sql new file mode 100644 index 000000000..f6f8e6854 --- /dev/null +++ b/db/src/main/resources/sql/flyway/V20__add_relock_duration.sql @@ -0,0 +1,15 @@ + -- Copyright 2020 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. + +ALTER TABLE "RegistryLock" ADD COLUMN relock_duration bigint; diff --git a/db/src/main/resources/sql/schema/db-schema.sql.generated b/db/src/main/resources/sql/schema/db-schema.sql.generated index fb832fa7e..c3a12e586 100644 --- a/db/src/main/resources/sql/schema/db-schema.sql.generated +++ b/db/src/main/resources/sql/schema/db-schema.sql.generated @@ -176,6 +176,7 @@ lock_request_timestamp timestamptz not null, registrar_id text not null, registrar_poc_id text, + relock_duration int8, repo_id text not null, unlock_completion_timestamp timestamptz, unlock_request_timestamp timestamptz, diff --git a/db/src/main/resources/sql/schema/nomulus.golden.sql b/db/src/main/resources/sql/schema/nomulus.golden.sql index f8b9554b9..5186fa432 100644 --- a/db/src/main/resources/sql/schema/nomulus.golden.sql +++ b/db/src/main/resources/sql/schema/nomulus.golden.sql @@ -262,7 +262,8 @@ CREATE TABLE public."RegistryLock" ( unlock_request_timestamp timestamp with time zone, unlock_completion_timestamp timestamp with time zone, last_update_timestamp timestamp with time zone, - relock_revision_id bigint + relock_revision_id bigint, + relock_duration bigint );