diff --git a/core/src/main/java/google/registry/model/registry/RegistryLockDao.java b/core/src/main/java/google/registry/model/registry/RegistryLockDao.java new file mode 100644 index 000000000..08d38538d --- /dev/null +++ b/core/src/main/java/google/registry/model/registry/RegistryLockDao.java @@ -0,0 +1,52 @@ +// Copyright 2019 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.model.registry; + +import static com.google.common.base.Preconditions.checkNotNull; +import static google.registry.model.transaction.TransactionManagerFactory.jpaTm; + +import google.registry.schema.domain.RegistryLock; +import javax.persistence.EntityManager; + +/** Data access object for {@link google.registry.schema.domain.RegistryLock}. */ +public final class RegistryLockDao { + + /** + * Returns the most recent version of the {@link RegistryLock} referred to by the verification + * code (there may be two instances of the same code in the database--one after lock object + * creation and one after verification. + */ + public static RegistryLock getByVerificationCode(String verificationCode) { + return jpaTm() + .transact( + () -> { + EntityManager em = jpaTm().getEntityManager(); + Long revisionId = + em.createQuery( + "SELECT MAX(revisionId) FROM RegistryLock WHERE verificationCode =" + + " :verificationCode", + Long.class) + .setParameter("verificationCode", verificationCode) + .getSingleResult(); + checkNotNull(revisionId, "No registry lock with this code"); + return em.find(RegistryLock.class, revisionId); + }); + } + + public static void save(RegistryLock registryLock) { + checkNotNull(registryLock, "Null registry lock cannot be saved"); + jpaTm().transact(() -> jpaTm().getEntityManager().persist(registryLock)); + } +} 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 80e32d6f8..17ff4df83 100644 --- a/core/src/main/java/google/registry/schema/domain/RegistryLock.java +++ b/core/src/main/java/google/registry/schema/domain/RegistryLock.java @@ -15,16 +15,18 @@ package google.registry.schema.domain; import static com.google.common.base.Preconditions.checkArgument; -import static google.registry.util.DateTimeUtils.toJodaDateTime; import static google.registry.util.DateTimeUtils.toZonedDateTime; import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; import google.registry.model.Buildable; +import google.registry.model.CreateAutoTimestamp; import google.registry.model.ImmutableObject; +import google.registry.persistence.CreateAutoTimestampConverter; import google.registry.util.DateTimeUtils; import java.time.ZonedDateTime; import java.util.Optional; import javax.persistence.Column; +import javax.persistence.Convert; import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; @@ -56,14 +58,17 @@ import org.joda.time.DateTime; * Unique constraint to get around Hibernate's failure to handle auto-increment field in * composite primary key. * - *
Note: because of this index, physical columns must be declared in the {@link Column} - * annotations for {@link RegistryLock#revisionId} and {@link RegistryLock#repoId} fields. + *
Note: indexes use the camelCase version of the field names because the {@link + * google.registry.persistence.NomulusNamingStrategy} does not translate the field name into the + * snake_case column name until the write itself. */ - indexes = - @Index( - name = "idx_registry_lock_repo_id_revision_id", - columnList = "repo_id, revision_id", - unique = true)) + indexes = { + @Index( + name = "idx_registry_lock_repo_id_revision_id", + columnList = "repoId, revisionId", + unique = true), + @Index(name = "idx_registry_lock_verification_code", columnList = "verificationCode") + }) public final class RegistryLock extends ImmutableObject implements Buildable { /** Describes the action taken by the user. */ @@ -74,11 +79,11 @@ public final class RegistryLock extends ImmutableObject implements Buildable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "revision_id", nullable = false) + @Column(nullable = false) private Long revisionId; /** EPP repo ID of the domain in question. */ - @Column(name = "repo_id", nullable = false) + @Column(nullable = false) private String repoId; // TODO (b/140568328): remove this when everything is in Cloud SQL and we can join on "domain" @@ -104,7 +109,8 @@ public final class RegistryLock extends ImmutableObject implements Buildable { /** Creation timestamp is when the lock/unlock is first requested. */ @Column(nullable = false) - private ZonedDateTime creationTimestamp; + @Convert(converter = CreateAutoTimestampConverter.class) + private CreateAutoTimestamp creationTimestamp = CreateAutoTimestamp.create(null); /** * Completion timestamp is when the user has verified the lock/unlock, when this object de facto @@ -148,7 +154,7 @@ public final class RegistryLock extends ImmutableObject implements Buildable { } public DateTime getCreationTimestamp() { - return toJodaDateTime(creationTimestamp); + return creationTimestamp.getTimestamp(); } /** Returns the completion timestamp, or empty if this lock has not been completed yet. */ @@ -168,9 +174,16 @@ public final class RegistryLock extends ImmutableObject implements Buildable { return revisionId; } + public void setCompletionTimestamp(DateTime dateTime) { + this.completionTimestamp = toZonedDateTime(dateTime); + } + @Override public Builder asBuilder() { - return new Builder(clone(this)); + RegistryLock clone = clone(this); + // Revision ID should be different for every object + clone.revisionId = null; + return new Builder(clone); } /** Builder for {@link google.registry.schema.domain.RegistryLock}. */ @@ -187,7 +200,6 @@ public final class RegistryLock extends ImmutableObject implements Buildable { checkArgumentNotNull(getInstance().domainName, "Domain name cannot be null"); checkArgumentNotNull(getInstance().registrarId, "Registrar ID cannot be null"); checkArgumentNotNull(getInstance().action, "Action cannot be null"); - checkArgumentNotNull(getInstance().creationTimestamp, "Creation timestamp cannot be null"); checkArgumentNotNull(getInstance().verificationCode, "Verification codecannot be null"); checkArgument( getInstance().registrarPocId != null || getInstance().isSuperuser, @@ -220,8 +232,8 @@ public final class RegistryLock extends ImmutableObject implements Buildable { return this; } - public Builder setCreationTimestamp(DateTime creationTimestamp) { - getInstance().creationTimestamp = toZonedDateTime(creationTimestamp); + public Builder setCreationTimestamp(CreateAutoTimestamp creationTimestamp) { + getInstance().creationTimestamp = creationTimestamp; return this; } diff --git a/core/src/test/java/google/registry/model/registry/RegistryLockDaoTest.java b/core/src/test/java/google/registry/model/registry/RegistryLockDaoTest.java new file mode 100644 index 000000000..4f2922e57 --- /dev/null +++ b/core/src/test/java/google/registry/model/registry/RegistryLockDaoTest.java @@ -0,0 +1,104 @@ +// Copyright 2019 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.model.registry; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.model.transaction.TransactionManagerFactory.jpaTm; +import static google.registry.testing.JUnitBackports.assertThrows; + +import google.registry.model.transaction.JpaTransactionManagerRule; +import google.registry.schema.domain.RegistryLock; +import google.registry.schema.domain.RegistryLock.Action; +import google.registry.testing.AppEngineRule; +import java.util.UUID; +import javax.persistence.PersistenceException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link RegistryLockDao}. */ +@RunWith(JUnit4.class) +public final class RegistryLockDaoTest { + + @Rule public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().build(); + + @Rule + public final JpaTransactionManagerRule jpaTmRule = + new JpaTransactionManagerRule.Builder().withEntityClass(RegistryLock.class).build(); + + @Test + public void testSaveAndLoad_success() { + RegistryLock lock = createLock(); + RegistryLockDao.save(lock); + RegistryLock fromDatabase = RegistryLockDao.getByVerificationCode(lock.getVerificationCode()); + assertThat(fromDatabase.getDomainName()).isEqualTo(lock.getDomainName()); + assertThat(fromDatabase.getVerificationCode()).isEqualTo(lock.getVerificationCode()); + } + + @Test + public void testSaveAndLoad_failure_differentCode() { + RegistryLock lock = createLock(); + RegistryLockDao.save(lock); + PersistenceException exception = + assertThrows( + PersistenceException.class, + () -> RegistryLockDao.getByVerificationCode(UUID.randomUUID().toString())); + assertThat(exception) + .hasCauseThat() + .hasMessageThat() + .isEqualTo("No registry lock with this code"); + assertThat(exception).hasCauseThat().isInstanceOf(NullPointerException.class); + } + + @Test + public void testSaveTwiceAndLoad_returnsLatest() { + RegistryLock lock = createLock(); + jpaTm().transact(() -> RegistryLockDao.save(lock)); + jpaTmRule.getTxnClock().advanceOneMilli(); + jpaTm() + .transact( + () -> { + RegistryLock secondLock = + RegistryLockDao.getByVerificationCode(lock.getVerificationCode()); + secondLock.setCompletionTimestamp(jpaTmRule.getTxnClock().nowUtc()); + RegistryLockDao.save(secondLock); + }); + jpaTm() + .transact( + () -> { + RegistryLock fromDatabase = + RegistryLockDao.getByVerificationCode(lock.getVerificationCode()); + assertThat(fromDatabase.getCompletionTimestamp().get()) + .isEqualTo(jpaTmRule.getTxnClock().nowUtc()); + }); + } + + @Test + public void testFailure_saveNull() { + assertThrows(NullPointerException.class, () -> RegistryLockDao.save(null)); + } + + private RegistryLock createLock() { + return new RegistryLock.Builder() + .setRepoId("repoId") + .setDomainName("example.test") + .setRegistrarId("TheRegistrar") + .setAction(Action.LOCK) + .setVerificationCode(UUID.randomUUID().toString()) + .isSuperuser(true) + .build(); + } +} diff --git a/db/src/main/resources/sql/schema/registry_lock.sql b/db/src/main/resources/sql/flyway/V4__registry_lock_add_index_on_verification_code.sql similarity index 55% rename from db/src/main/resources/sql/schema/registry_lock.sql rename to db/src/main/resources/sql/flyway/V4__registry_lock_add_index_on_verification_code.sql index fa4457cc9..45b10cdfa 100644 --- a/db/src/main/resources/sql/schema/registry_lock.sql +++ b/db/src/main/resources/sql/flyway/V4__registry_lock_add_index_on_verification_code.sql @@ -12,19 +12,5 @@ -- See the License for the specific language governing permissions and -- limitations under the License. -CREATE TABLE "RegistryLock" ( - revision_id BIGSERIAL NOT NULL, - action TEXT NOT NULL, - completion_timestamp TIMESTAMPTZ, - creation_timestamp TIMESTAMPTZ NOT NULL, - domain_name TEXT NOT NULL, - is_superuser BOOLEAN NOT NULL, - registrar_id TEXT NOT NULL, - registrar_poc_id TEXT, - repo_id TEXT NOT NULL, - verification_code TEXT NOT NULL, - PRIMARY KEY (revision_id) -); - -ALTER TABLE IF EXISTS "RegistryLock" - ADD CONSTRAINT idx_registry_lock_repo_id_revision_id UNIQUE (repo_id, revision_id); + create index if not exists idx_registry_lock_verification_code ON "RegistryLock" + using btree (verification_code); diff --git a/db/src/main/resources/sql/schema/nomulus.golden.sql b/db/src/main/resources/sql/schema/nomulus.golden.sql index bcaffbb08..c20712d86 100644 --- a/db/src/main/resources/sql/schema/nomulus.golden.sql +++ b/db/src/main/resources/sql/schema/nomulus.golden.sql @@ -220,6 +220,13 @@ ALTER TABLE ONLY public."RegistryLock" ADD CONSTRAINT idx_registry_lock_repo_id_revision_id UNIQUE (repo_id, revision_id); +-- +-- Name: idx_registry_lock_verification_code; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_registry_lock_verification_code ON public."RegistryLock" USING btree (verification_code); + + -- -- Name: ClaimsEntry fk6sc6at5hedffc0nhdcab6ivuq; Type: FK CONSTRAINT; Schema: public; Owner: - --