diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9ead6d7..54d3d99 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -40,4 +40,4 @@ jobs: tag-name: 'v${{ github.event.inputs.version }}' - uses: aboutbits/github-actions-base/github-create-release@v2 with: - tag-name: 'v${{ github.event.inputs.version }}' + tag-name: '${{ github.event.inputs.version }}' diff --git a/pom.xml b/pom.xml index 94e82fb..856b58b 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ it.aboutbits spring-boot-toolbox - 1.1.0 + 1.3.0 diff --git a/src/main/java/it/aboutbits/springboot/testing/persistence/PersistenceAssert.java b/src/main/java/it/aboutbits/springboot/testing/persistence/PersistenceAssert.java index 2a87018..e15ab6b 100644 --- a/src/main/java/it/aboutbits/springboot/testing/persistence/PersistenceAssert.java +++ b/src/main/java/it/aboutbits/springboot/testing/persistence/PersistenceAssert.java @@ -9,6 +9,13 @@ import lombok.NonNull; import lombok.RequiredArgsConstructor; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import static it.aboutbits.springboot.toolbox.util.CollectUtil.collectToSet; import static org.assertj.core.api.Assertions.assertThat; public final class PersistenceAssert { @@ -23,115 +30,224 @@ private static EntityManager getEntityManager() { public static , E extends Identified & ChangeAware, M extends Identified & ChangeAware> WriteOperationAsserter assertThatEntity( @NonNull E before, @NonNull Class modelClass + ) { + return new WriteOperationAsserter<>(getEntityManager(), List.of(before), modelClass); + } + + @SuppressWarnings("unused") + public static , E extends Identified & ChangeAware, M extends Identified & ChangeAware> WriteOperationAsserter assertThatEntity( + @NonNull Collection before, + @NonNull Class modelClass ) { return new WriteOperationAsserter<>(getEntityManager(), before, modelClass); } + /** + * @deprecated Use {@link #assertThatEntityId(EntityId, Class)} instead. + */ + @Deprecated @SuppressWarnings("unused") public static , M extends Identified> WriteOperationIdAsserter assertThatEntity( @NonNull ID id, @NonNull Class modelClass + ) { + return new WriteOperationIdAsserter<>(getEntityManager(), List.of(id), modelClass); + } + + @SuppressWarnings("unused") + public static , M extends Identified> WriteOperationIdAsserter assertThatEntityId( + @NonNull ID id, + @NonNull Class modelClass + ) { + return new WriteOperationIdAsserter<>(getEntityManager(), List.of(id), modelClass); + } + + @SuppressWarnings("unused") + public static , M extends Identified> WriteOperationIdAsserter assertThatEntityId( + @NonNull Collection id, + @NonNull Class modelClass ) { return new WriteOperationIdAsserter<>(getEntityManager(), id, modelClass); } + /** + * Batch query entities by their IDs using JPQL + */ + private static , M extends Identified> List batchFindByIds( + EntityManager entityManager, + Collection ids, + Class modelClass + ) { + if (ids.isEmpty()) { + return List.of(); + } + + return ids.stream() + .map( + id -> entityManager.find(modelClass, id) + ) + .filter(Objects::nonNull) + .toList(); + } + + /** + * Check if entities with given IDs exist in the database using a count query + */ + private static , M extends Identified> Map batchCheckExistence( + EntityManager entityManager, + Collection ids, + Class modelClass + ) { + if (ids.isEmpty()) { + return Map.of(); + } + + // Get existing entities + var existingEntities = batchFindByIds(entityManager, ids, modelClass); + var existingIds = collectToSet(existingEntities, Identified::getId); + + // Map each ID to its existence status + return ids.stream() + .collect(Collectors.toMap( + id -> id, + existingIds::contains + )); + } + @RequiredArgsConstructor(access = AccessLevel.PRIVATE) public static final class WriteOperationAsserter, E extends Identified & ChangeAware, M extends Identified & ChangeAware> { private final EntityManager entityManager; - private final E entity; + private final Collection entity; private final Class modelClass; @SuppressWarnings("unused") public void hasBeenCreatedInDatabase() { entityManager.clear(); - var savedInstance = getSavedInstance(); - assertThat( - savedInstance.getId() - ).isNotNull(); + var ids = collectToSet(entity, Identified::getId); + var savedInstances = batchFindByIds(entityManager, ids, modelClass); + + for (var savedInstance : savedInstances) { + assertThat( + savedInstance.getId() + ).isNotNull(); + + assertThat( + savedInstance.getCreatedAt() + ).isNotNull(); + } - assertThat( - savedInstance.getCreatedAt() - ).isNotNull(); + // Verify all entities were found + assertThat(savedInstances).hasSize(entity.size()); } @SuppressWarnings("unused") public void hasBeenUpdatedInDatabase() { entityManager.clear(); - var savedInstance = getSavedInstance(); - assertThat( - savedInstance.getUpdatedAt() - ).isAfter( - entity.getUpdatedAt() - ); + var ids = collectToSet(entity, Identified::getId); + var savedInstances = batchFindByIds(entityManager, ids, modelClass); + + // Create a map for easy lookup of original entities by ID + var originalByIdMap = entity.stream() + .collect(Collectors.toMap(Identified::getId, e -> e)); + + for (var savedInstance : savedInstances) { + var originalEntity = originalByIdMap.get(savedInstance.getId()); + assertThat(originalEntity).isNotNull(); + + assertThat( + savedInstance.getUpdatedAt() + ).isAfter( + originalEntity.getUpdatedAt() + ); + } } @SuppressWarnings("unused") public void hasNotChangedInDatabase() { entityManager.clear(); - var savedInstance = getSavedInstance(); - assertThat(savedInstance).isNotNull(); + var ids = collectToSet(entity, Identified::getId); + var savedInstances = batchFindByIds(entityManager, ids, modelClass); + + // Create a map for easy lookup of original entities by ID + var originalByIdMap = entity.stream() + .collect(Collectors.toMap(Identified::getId, e -> e)); + + for (var savedInstance : savedInstances) { + var originalEntity = originalByIdMap.get(savedInstance.getId()); - assertThat( - savedInstance.getUpdatedAt() - ).isEqualTo( - entity.getUpdatedAt() - ); + assertThat(savedInstance).isNotNull(); + assertThat(originalEntity).isNotNull(); + + assertThat( + savedInstance.getUpdatedAt() + ).isEqualTo( + originalEntity.getUpdatedAt() + ); + } } @SuppressWarnings("unused") public void isAbsentInDatabase() { entityManager.clear(); - var savedInstance = getSavedInstance(); - assertThat(savedInstance).isNull(); + var ids = collectToSet(entity, Identified::getId); + var existenceMap = batchCheckExistence(entityManager, ids, modelClass); + + for (var id : ids) { + assertThat(existenceMap.get(id)) + .as("Entity with ID %s should be absent from database", id) + .isFalse(); + } } @SuppressWarnings("unused") public void isPresentInDatabase() { entityManager.clear(); - var savedInstance = getSavedInstance(); - assertThat(savedInstance).isNotNull(); - } + var ids = collectToSet(entity, Identified::getId); + var existenceMap = batchCheckExistence(entityManager, ids, modelClass); - private M getSavedInstance() { - return entityManager.find( - modelClass, - entity.getId() - ); + for (var id : ids) { + assertThat(existenceMap.get(id)) + .as("Entity with ID %s should be present in database", id) + .isTrue(); + } } } @RequiredArgsConstructor(access = AccessLevel.PRIVATE) public static final class WriteOperationIdAsserter, M extends Identified> { private final EntityManager entityManager; - private final ID id; + private final Collection id; private final Class modelClass; @SuppressWarnings("unused") public void isAbsentInDatabase() { entityManager.clear(); - var savedInstance = getSavedInstance(); - assertThat(savedInstance).isNull(); + var existenceMap = batchCheckExistence(entityManager, id, modelClass); + + for (var entityId : id) { + assertThat(existenceMap.get(entityId)) + .as("Entity with ID %s should be absent from database", entityId) + .isFalse(); + } } @SuppressWarnings("unused") public void isPresentInDatabase() { entityManager.clear(); - var savedInstance = getSavedInstance(); - assertThat(savedInstance).isNotNull(); - } + var existenceMap = batchCheckExistence(entityManager, id, modelClass); - private M getSavedInstance() { - return entityManager.find( - modelClass, - id - ); + for (var entityId : id) { + assertThat(existenceMap.get(entityId)) + .as("Entity with ID %s should be present in database", entityId) + .isTrue(); + } } } } diff --git a/src/main/java/it/aboutbits/springboot/testing/testdata/base/AllTestDataReader.java b/src/main/java/it/aboutbits/springboot/testing/testdata/base/AllTestDataReader.java new file mode 100644 index 0000000..bd3e6b4 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/testdata/base/AllTestDataReader.java @@ -0,0 +1,68 @@ +package it.aboutbits.springboot.testing.testdata.base; + +import lombok.NonNull; + +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; + +public abstract class AllTestDataReader { + public ITEM returnFirst() { + return (ITEM) this.returnAll().getFirst(); + } + + public List returnAll() { + return this.fetch(); + } + + @SafeVarargs + public final List returnSorted(@NonNull Comparator... comparators) { + if (comparators.length == 0) { + throw new IllegalArgumentException("At least one comparator must be provided"); + } + + var combinedComparator = comparators[0]; + for (var i = 1; i < comparators.length; i++) { + combinedComparator = combinedComparator.thenComparing(comparators[i]); + } + + return returnAll().stream().sorted(combinedComparator).toList(); + } + + @SafeVarargs + @SuppressWarnings("unchecked") + public final > List returnSorted(@NonNull Function>... comparators) { + if (comparators.length == 0) { + throw new IllegalArgumentException("At least one comparator must be provided"); + } + + var combinedComparator = Comparator.comparing((Function) comparators[0]); + for (var i = 1; i < comparators.length; i++) { + combinedComparator = combinedComparator.thenComparing((Function) comparators[i]); + } + + return returnAll().stream().sorted(combinedComparator).toList(); + } + + public > AllAndFiltered returnFiltered(@NonNull Predicate predicate) { + var all = this.returnAll(); + return new AllAndFiltered( + all, + all.stream().filter(predicate).toList(), + all.stream().filter(item -> !predicate.test(item)).toList() + ); + } + + public Set returnSet() { + return new HashSet<>(this.returnAll()); + } + + protected abstract List fetch(); + + public record AllAndFiltered(@NonNull List all, @NonNull List filtered, @NonNull List other) { + + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/testdata/base/TestDataCreator.java b/src/main/java/it/aboutbits/springboot/testing/testdata/base/TestDataCreator.java index bcd3c08..e9ef674 100644 --- a/src/main/java/it/aboutbits/springboot/testing/testdata/base/TestDataCreator.java +++ b/src/main/java/it/aboutbits/springboot/testing/testdata/base/TestDataCreator.java @@ -30,14 +30,33 @@ public List returnAll() { return create(); } - public List returnSorted(@NonNull Comparator comparator) { - return returnAll().stream().sorted(comparator).toList(); + @SafeVarargs + public final List returnSorted(@NonNull Comparator... comparators) { + if (comparators.length == 0) { + throw new IllegalArgumentException("At least one comparator must be provided"); + } + + var combinedComparator = comparators[0]; + for (var i = 1; i < comparators.length; i++) { + combinedComparator = combinedComparator.thenComparing(comparators[i]); + } + + return returnAll().stream().sorted(combinedComparator).toList(); } - public > List returnSorted(@NonNull Function comparator) { - return returnAll().stream() - .sorted(Comparator.comparing(comparator)) - .toList(); + @SafeVarargs + @SuppressWarnings("unchecked") + public final > List returnSorted(@NonNull Function>... comparators) { + if (comparators.length == 0) { + throw new IllegalArgumentException("At least one comparator must be provided"); + } + + var combinedComparator = Comparator.comparing((Function) comparators[0]); + for (var i = 1; i < comparators.length; i++) { + combinedComparator = combinedComparator.thenComparing((Function) comparators[i]); + } + + return returnAll().stream().sorted(combinedComparator).toList(); } public Set returnSet() {