// Copyright 2017 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.ofy; import static com.google.appengine.api.datastore.EntityTranslator.convertToPb; import static com.google.common.truth.Truth.assertThat; import static com.googlecode.objectify.ObjectifyService.register; import static google.registry.model.common.EntityGroupRoot.getCrossTldKey; import static google.registry.model.ofy.CommitLogBucket.getBucketKey; import static google.registry.model.ofy.ObjectifyService.ofy; import com.google.common.collect.ImmutableSet; import com.googlecode.objectify.Key; import com.googlecode.objectify.annotation.Entity; import com.googlecode.objectify.annotation.Id; import com.googlecode.objectify.annotation.Parent; import google.registry.model.BackupGroupRoot; import google.registry.model.ImmutableObject; import google.registry.model.common.EntityGroupRoot; import google.registry.testing.AppEngineRule; import google.registry.testing.ExceptionRule; import google.registry.testing.FakeClock; import google.registry.testing.InjectRule; import google.registry.testing.TestObject.TestVirtualObject; import org.joda.time.DateTime; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Unit tests ensuring {@link Ofy} saves transactions to {@link CommitLogManifest}. */ @RunWith(JUnit4.class) public class OfyCommitLogTest { @Rule public final AppEngineRule appEngine = AppEngineRule.builder() .withDatastore() .build(); @Rule public final ExceptionRule thrown = new ExceptionRule(); @Rule public final InjectRule inject = new InjectRule(); private final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01TZ")); @Before public void before() throws Exception { register(Root.class); register(Child.class); inject.setStaticField(Ofy.class, "clock", clock); } @Test public void testTransact_doesNothing_noCommitLogIsSaved() throws Exception { ofy().transact(() -> {}); assertThat(ofy().load().type(CommitLogManifest.class)).isEmpty(); } @Test public void testTransact_savesDataAndCommitLog() throws Exception { ofy().transact(() -> ofy().save().entity(Root.create(1, getCrossTldKey())).now()); assertThat(ofy().load().key(Key.create(getCrossTldKey(), Root.class, 1)).now().value) .isEqualTo("value"); assertThat(ofy().load().type(CommitLogManifest.class)).hasSize(1); assertThat(ofy().load().type(CommitLogMutation.class)).hasSize(1); } @Test public void testTransact_saveWithoutBackup_noCommitLogIsSaved() throws Exception { ofy().transact(() -> ofy().saveWithoutBackup().entity(Root.create(1, getCrossTldKey())).now()); assertThat(ofy().load().key(Key.create(getCrossTldKey(), Root.class, 1)).now().value) .isEqualTo("value"); assertThat(ofy().load().type(CommitLogManifest.class)).isEmpty(); assertThat(ofy().load().type(CommitLogMutation.class)).isEmpty(); } @Test public void testTransact_deleteWithoutBackup_noCommitLogIsSaved() throws Exception { ofy().transact(() -> ofy().saveWithoutBackup().entity(Root.create(1, getCrossTldKey())).now()); ofy().transact(() -> ofy().deleteWithoutBackup().key(Key.create(Root.class, 1))); assertThat(ofy().load().key(Key.create(Root.class, 1)).now()).isNull(); assertThat(ofy().load().type(CommitLogManifest.class)).isEmpty(); assertThat(ofy().load().type(CommitLogMutation.class)).isEmpty(); } @Test public void testTransact_savesEntity_itsProtobufFormIsStoredInCommitLog() throws Exception { ofy().transact(() -> ofy().save().entity(Root.create(1, getCrossTldKey())).now()); final byte[] entityProtoBytes = ofy().load().type(CommitLogMutation.class).first().now().entityProtoBytes; // This transaction is needed so that save().toEntity() can access ofy().getTransactionTime() // when it attempts to set the update timestamp. ofy() .transact( () -> assertThat(entityProtoBytes) .isEqualTo( convertToPb(ofy().save().toEntity(Root.create(1, getCrossTldKey()))) .toByteArray())); } @Test public void testTransact_savesEntity_mutationIsChildOfManifest() throws Exception { ofy().transact(() -> ofy().save().entity(Root.create(1, getCrossTldKey())).now()); assertThat( ofy() .load() .type(CommitLogMutation.class) .ancestor(ofy().load().type(CommitLogManifest.class).first().now())) .hasSize(1); } @Test public void testTransactNew_savesDataAndCommitLog() throws Exception { ofy().transactNew(() -> ofy().save().entity(Root.create(1, getCrossTldKey())).now()); assertThat(ofy().load().key(Key.create(getCrossTldKey(), Root.class, 1)).now().value) .isEqualTo("value"); assertThat(ofy().load().type(CommitLogManifest.class)).hasSize(1); assertThat(ofy().load().type(CommitLogMutation.class)).hasSize(1); } @Test public void testTransact_multipleSaves_logsMultipleMutations() throws Exception { ofy() .transact( () -> { ofy().save().entity(Root.create(1, getCrossTldKey())).now(); ofy().save().entity(Root.create(2, getCrossTldKey())).now(); }); assertThat(ofy().load().type(CommitLogManifest.class)).hasSize(1); assertThat(ofy().load().type(CommitLogMutation.class)).hasSize(2); } @Test public void testTransact_deletion_deletesAndLogsWithoutMutation() throws Exception { ofy().transact(() -> ofy().saveWithoutBackup().entity(Root.create(1, getCrossTldKey())).now()); clock.advanceOneMilli(); final Key otherTldKey = Key.create(getCrossTldKey(), Root.class, 1); ofy().transact(() -> ofy().delete().key(otherTldKey)); assertThat(ofy().load().key(otherTldKey).now()).isNull(); assertThat(ofy().load().type(CommitLogManifest.class)).hasSize(1); assertThat(ofy().load().type(CommitLogMutation.class)).isEmpty(); assertThat(ofy().load().type(CommitLogManifest.class).first().now().getDeletions()) .containsExactly(otherTldKey); } @Test public void testTransactNew_deleteNotBackedUpKind_throws() throws Exception { final CommitLogManifest backupsArentAllowedOnMe = CommitLogManifest.create(getBucketKey(1), clock.nowUtc(), ImmutableSet.>of()); thrown.expect(IllegalArgumentException.class, "Can't save/delete a @NotBackedUp"); ofy().transactNew(() -> ofy().delete().entity(backupsArentAllowedOnMe)); } @Test public void testTransactNew_saveNotBackedUpKind_throws() throws Exception { final CommitLogManifest backupsArentAllowedOnMe = CommitLogManifest.create(getBucketKey(1), clock.nowUtc(), ImmutableSet.>of()); thrown.expect(IllegalArgumentException.class, "Can't save/delete a @NotBackedUp"); ofy().transactNew(() -> ofy().save().entity(backupsArentAllowedOnMe)); } @Test public void testTransactNew_deleteVirtualEntityKey_throws() throws Exception { final Key virtualEntityKey = TestVirtualObject.createKey("virtual"); thrown.expect(IllegalArgumentException.class, "Can't save/delete a @VirtualEntity"); ofy().transactNew(() -> ofy().delete().key(virtualEntityKey)); } @Test public void testTransactNew_saveVirtualEntity_throws() throws Exception { final TestVirtualObject virtualEntity = TestVirtualObject.create("virtual"); thrown.expect(IllegalArgumentException.class, "Can't save/delete a @VirtualEntity"); ofy().transactNew(() -> ofy().save().entity(virtualEntity)); } @Test public void test_deleteWithoutBackup_withVirtualEntityKey_throws() throws Exception { final Key virtualEntityKey = TestVirtualObject.createKey("virtual"); thrown.expect(IllegalArgumentException.class, "Can't save/delete a @VirtualEntity"); ofy().deleteWithoutBackup().key(virtualEntityKey); } @Test public void test_saveWithoutBackup_withVirtualEntity_throws() throws Exception { final TestVirtualObject virtualEntity = TestVirtualObject.create("virtual"); thrown.expect(IllegalArgumentException.class, "Can't save/delete a @VirtualEntity"); ofy().saveWithoutBackup().entity(virtualEntity); } @Test public void testTransact_twoSavesOnSameKey_throws() throws Exception { thrown.expect(IllegalArgumentException.class, "Multiple entries with same key"); ofy() .transact( () -> { ofy().save().entity(Root.create(1, getCrossTldKey())); ofy().save().entity(Root.create(1, getCrossTldKey())); }); } @Test public void testTransact_saveAndDeleteSameKey_throws() throws Exception { thrown.expect(IllegalArgumentException.class, "Multiple entries with same key"); ofy() .transact( () -> { ofy().save().entity(Root.create(1, getCrossTldKey())); ofy().delete().entity(Root.create(1, getCrossTldKey())); }); } @Test public void testSavingRootAndChild_updatesTimestampOnBackupGroupRoot() throws Exception { ofy().transact(() -> ofy().save().entity(Root.create(1, getCrossTldKey()))); ofy().clearSessionCache(); assertThat(ofy().load().key(Key.create(getCrossTldKey(), Root.class, 1)).now() .getUpdateAutoTimestamp().getTimestamp()).isEqualTo(clock.nowUtc()); clock.advanceOneMilli(); ofy() .transact( () -> { ofy().save().entity(Root.create(1, getCrossTldKey())); ofy().save().entity(new Child()); }); ofy().clearSessionCache(); assertThat(ofy().load().key(Key.create(getCrossTldKey(), Root.class, 1)).now() .getUpdateAutoTimestamp().getTimestamp()).isEqualTo(clock.nowUtc()); } @Test public void testSavingOnlyChild_updatesTimestampOnBackupGroupRoot() throws Exception { ofy().transact(() -> ofy().save().entity(Root.create(1, getCrossTldKey()))); ofy().clearSessionCache(); assertThat(ofy().load().key(Key.create(getCrossTldKey(), Root.class, 1)).now() .getUpdateAutoTimestamp().getTimestamp()).isEqualTo(clock.nowUtc()); clock.advanceOneMilli(); ofy().transact(() -> ofy().save().entity(new Child())); ofy().clearSessionCache(); assertThat(ofy().load().key(Key.create(getCrossTldKey(), Root.class, 1)).now() .getUpdateAutoTimestamp().getTimestamp()).isEqualTo(clock.nowUtc()); } @Test public void testDeletingChild_updatesTimestampOnBackupGroupRoot() throws Exception { ofy().transact(() -> ofy().save().entity(Root.create(1, getCrossTldKey()))); ofy().clearSessionCache(); assertThat(ofy().load().key(Key.create(getCrossTldKey(), Root.class, 1)).now() .getUpdateAutoTimestamp().getTimestamp()).isEqualTo(clock.nowUtc()); clock.advanceOneMilli(); // The fact that the child was never persisted is irrelevant. ofy().transact(() -> ofy().delete().entity(new Child())); ofy().clearSessionCache(); assertThat(ofy().load().key(Key.create(getCrossTldKey(), Root.class, 1)).now() .getUpdateAutoTimestamp().getTimestamp()).isEqualTo(clock.nowUtc()); } @Test public void testReadingRoot_doesntUpdateTimestamp() throws Exception { ofy().transact(() -> ofy().save().entity(Root.create(1, getCrossTldKey()))); ofy().clearSessionCache(); assertThat(ofy().load().key(Key.create(getCrossTldKey(), Root.class, 1)).now() .getUpdateAutoTimestamp().getTimestamp()).isEqualTo(clock.nowUtc()); clock.advanceOneMilli(); ofy() .transact( () -> { // Don't remove this line, as without saving *something* the commit log code will // never be invoked and the test will trivially pass. ofy().save().entity(Root.create(2, getCrossTldKey())); ofy().load().entity(Root.create(1, getCrossTldKey())); }); ofy().clearSessionCache(); assertThat(ofy().load().key(Key.create(getCrossTldKey(), Root.class, 1)).now() .getUpdateAutoTimestamp().getTimestamp()).isEqualTo(clock.nowUtc().minusMillis(1)); } @Test public void testReadingChild_doesntUpdateTimestampOnBackupGroupRoot() throws Exception { ofy().transact(() -> ofy().save().entity(Root.create(1, getCrossTldKey()))); ofy().clearSessionCache(); assertThat(ofy().load().key(Key.create(getCrossTldKey(), Root.class, 1)).now() .getUpdateAutoTimestamp().getTimestamp()).isEqualTo(clock.nowUtc()); clock.advanceOneMilli(); ofy() .transact( () -> { // Don't remove this line, as without saving *something* the commit log code will // never be invoked and the test will trivially pass ofy().save().entity(Root.create(2, getCrossTldKey())); ofy().load().entity(new Child()); // All Child objects are under Root(1). }); ofy().clearSessionCache(); assertThat(ofy().load().key(Key.create(getCrossTldKey(), Root.class, 1)).now() .getUpdateAutoTimestamp().getTimestamp()).isEqualTo(clock.nowUtc().minusMillis(1)); } @Test public void testSavingAcrossBackupGroupRoots_updatesCorrectTimestamps() throws Exception { // Create three roots. ofy() .transact( () -> { ofy().save().entity(Root.create(1, getCrossTldKey())); ofy().save().entity(Root.create(2, getCrossTldKey())); ofy().save().entity(Root.create(3, getCrossTldKey())); }); ofy().clearSessionCache(); for (int i = 1; i <= 3; i++) { assertThat(ofy().load().key(Key.create(getCrossTldKey(), Root.class, i)).now() .getUpdateAutoTimestamp().getTimestamp()).isEqualTo(clock.nowUtc()); } clock.advanceOneMilli(); // Mutate one root, and a child of a second, ignoring the third. ofy() .transact( () -> { ofy().save().entity(new Child()); // All Child objects are under Root(1). ofy().save().entity(Root.create(2, getCrossTldKey())); }); ofy().clearSessionCache(); // Child was touched. assertThat(ofy().load().key(Key.create(getCrossTldKey(), Root.class, 1)).now() .getUpdateAutoTimestamp().getTimestamp()).isEqualTo(clock.nowUtc()); // Directly touched. assertThat(ofy().load().key(Key.create(getCrossTldKey(), Root.class, 2)).now() .getUpdateAutoTimestamp().getTimestamp()).isEqualTo(clock.nowUtc()); // Wasn't touched. assertThat(ofy().load().key(Key.create(getCrossTldKey(), Root.class, 3)).now() .getUpdateAutoTimestamp().getTimestamp()).isEqualTo(clock.nowUtc().minusMillis(1)); } @Entity static class Root extends BackupGroupRoot { @Parent Key parent; @Id long id; String value; static Root create(long id, Key parent) { Root result = new Root(); result.parent = parent; result.id = id; result.value = "value"; return result; } } @Entity static class Child extends ImmutableObject { @Parent Key parent = Key.create(Root.create(1, getCrossTldKey())); @Id long id = 1; } }