From a04929453c33c7ccda32382caf58267e7fc79098 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sat, 20 Aug 2022 12:29:14 +0200 Subject: [PATCH] implements optimistic locking for PackageEntity --- .../hsadminng/hs/hspackage/PackageEntity.java | 3 + .../changelog/2022-07-29-070-hs-package.sql | 1 + .../PackageRepositoryIntegrationTest.java | 85 +++++++++++++++---- .../hsadminng/hs/hspackage/TestPackage.java | 2 +- 4 files changed, 72 insertions(+), 19 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hspackage/PackageEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/hspackage/PackageEntity.java index ff87a851..3d5d149e 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hspackage/PackageEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hspackage/PackageEntity.java @@ -19,6 +19,9 @@ public class PackageEntity { private @Id UUID uuid; + @Version + private int version; + @ManyToOne(optional = false) @JoinColumn(name = "customeruuid") private CustomerEntity customer; diff --git a/src/main/resources/db/changelog/2022-07-29-070-hs-package.sql b/src/main/resources/db/changelog/2022-07-29-070-hs-package.sql index 488d64b2..94a413d6 100644 --- a/src/main/resources/db/changelog/2022-07-29-070-hs-package.sql +++ b/src/main/resources/db/changelog/2022-07-29-070-hs-package.sql @@ -7,6 +7,7 @@ create table if not exists package ( uuid uuid unique references RbacObject (uuid), + version int not null default 0, customerUuid uuid references customer (uuid), name varchar(5), description varchar(96) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hspackage/PackageRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hspackage/PackageRepositoryIntegrationTest.java index b978fa86..e5a552a4 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hspackage/PackageRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hspackage/PackageRepositoryIntegrationTest.java @@ -2,11 +2,13 @@ package net.hostsharing.hsadminng.hs.hspackage; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.hscustomer.CustomerRepository; +import net.hostsharing.test.JpaAttempt; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.ComponentScan; +import org.springframework.orm.ObjectOptimisticLockingFailureException; import org.springframework.orm.jpa.JpaSystemException; import org.springframework.test.annotation.DirtiesContext; import org.springframework.transaction.annotation.Transactional; @@ -18,7 +20,7 @@ import static net.hostsharing.test.JpaAttempt.attempt; import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest -@ComponentScan(basePackageClasses = { Context.class, CustomerRepository.class }) +@ComponentScan(basePackageClasses = { Context.class, CustomerRepository.class, JpaAttempt.class }) @DirtiesContext class PackageRepositoryIntegrationTest { @@ -28,7 +30,11 @@ class PackageRepositoryIntegrationTest { @Autowired PackageRepository packageRepository; - @Autowired EntityManager em; + @Autowired + EntityManager em; + + @Autowired + JpaAttempt jpaAttempt; @Nested class FindAllByOptionalNameLike { @@ -88,13 +94,13 @@ class PackageRepositoryIntegrationTest { // when final var result = attempt( - em, - () -> packageRepository.findAllByOptionalNameLike(null)); + em, + () -> packageRepository.findAllByOptionalNameLike(null)); // then result.assertExceptionWithRootCauseMessage( - JpaSystemException.class, - "[403] user admin@aaa.example.com", "has no permission to assume role package#aab00#admin"); + JpaSystemException.class, + "[403] user admin@aaa.example.com", "has no permission to assume role package#aab00#admin"); } @Test @@ -102,12 +108,12 @@ class PackageRepositoryIntegrationTest { currentUser("unknown@example.org"); final var result = attempt( - em, - () -> packageRepository.findAllByOptionalNameLike(null)); + em, + () -> packageRepository.findAllByOptionalNameLike(null)); result.assertExceptionWithRootCauseMessage( - JpaSystemException.class, - "hsadminng.currentUser defined as unknown@example.org, but does not exists"); + JpaSystemException.class, + "hsadminng.currentUser defined as unknown@example.org, but does not exists"); } @Test @@ -117,16 +123,59 @@ class PackageRepositoryIntegrationTest { assumedRoles("customer#aaa.admin"); final var result = attempt( - em, - () -> packageRepository.findAllByOptionalNameLike(null)); + em, + () -> packageRepository.findAllByOptionalNameLike(null)); result.assertExceptionWithRootCauseMessage( - JpaSystemException.class, - "hsadminng.currentUser defined as unknown@example.org, but does not exists"); + JpaSystemException.class, + "hsadminng.currentUser defined as unknown@example.org, but does not exists"); } } + @Nested + class OptimisticLocking { + + @Test + public void supportsOptimisticLocking() throws InterruptedException { + // given + hostsharingAdminWithAssumedRole("package#aaa00.admin"); + final var pac = packageRepository.findAllByOptionalNameLike("%").get(0); + + // when + final var result1 = jpaAttempt.transacted(() -> { + hostsharingAdminWithAssumedRole("package#aaa00.admin"); + pac.setDescription("description set by thread 1"); + packageRepository.save(pac); + }); + final var result2 = jpaAttempt.transacted(() -> { + hostsharingAdminWithAssumedRole("package#aaa00.admin"); + pac.setDescription("description set by thread 2"); + packageRepository.save(pac); + sleep(1500); + }); + + // then + em.refresh(pac); + assertThat(pac.getDescription()).isEqualTo("description set by thread 1"); + assertThat(result1.caughtException()).isNull(); + assertThat(result2.caughtException()).isInstanceOf(ObjectOptimisticLockingFailureException.class); + } + + private void sleep(final int millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + private void hostsharingAdminWithAssumedRole(final String assumedRoles) { + currentUser("mike@hostsharing.net"); + assumedRoles(assumedRoles); + } + void currentUser(final String currentUser) { context.setCurrentUser(currentUser); assertThat(context.getCurrentUser()).as("precondition").isEqualTo(currentUser); @@ -139,14 +188,14 @@ class PackageRepositoryIntegrationTest { void noPackagesAreReturned(final List actualResult) { assertThat(actualResult) - .extracting(PackageEntity::getName) - .isEmpty(); + .extracting(PackageEntity::getName) + .isEmpty(); } void exactlyThesePackagesAreReturned(final List actualResult, final String... packageNames) { assertThat(actualResult) - .extracting(PackageEntity::getName) - .containsExactlyInAnyOrder(packageNames); + .extracting(PackageEntity::getName) + .containsExactlyInAnyOrder(packageNames); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hspackage/TestPackage.java b/src/test/java/net/hostsharing/hsadminng/hs/hspackage/TestPackage.java index 0444a836..bac92208 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hspackage/TestPackage.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hspackage/TestPackage.java @@ -12,6 +12,6 @@ public class TestPackage { public static final PackageEntity xxx02 = hsPackage(TestCustomer.xxx, "xxx02"); public static PackageEntity hsPackage(final CustomerEntity customer, final String name) { - return new PackageEntity(randomUUID(), customer, name, "initial description of package " + name); + return new PackageEntity(randomUUID(), 0, customer, name, "initial description of package " + name); } }