Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}'
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<dependency>
<groupId>it.aboutbits</groupId>
<artifactId>spring-boot-toolbox</artifactId>
<version>1.1.0</version>
<version>1.3.0</version>
</dependency>
</dependencies>
</dependencyManagement>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -23,115 +30,224 @@ private static EntityManager getEntityManager() {
public static <ID extends EntityId<?>, E extends Identified<ID> & ChangeAware, M extends Identified<ID> & ChangeAware> WriteOperationAsserter<ID, E, M> assertThatEntity(
@NonNull E before,
@NonNull Class<M> modelClass
) {
return new WriteOperationAsserter<>(getEntityManager(), List.of(before), modelClass);
}

@SuppressWarnings("unused")
public static <ID extends EntityId<?>, E extends Identified<ID> & ChangeAware, M extends Identified<ID> & ChangeAware> WriteOperationAsserter<ID, E, M> assertThatEntity(
@NonNull Collection<E> before,
@NonNull Class<M> modelClass
) {
return new WriteOperationAsserter<>(getEntityManager(), before, modelClass);
}

/**
* @deprecated Use {@link #assertThatEntityId(EntityId, Class)} instead.
*/
@Deprecated
@SuppressWarnings("unused")
public static <ID extends EntityId<?>, M extends Identified<ID>> WriteOperationIdAsserter<ID, M> assertThatEntity(
@NonNull ID id,
@NonNull Class<M> modelClass
) {
return new WriteOperationIdAsserter<>(getEntityManager(), List.of(id), modelClass);
}

@SuppressWarnings("unused")
public static <ID extends EntityId<?>, M extends Identified<ID>> WriteOperationIdAsserter<ID, M> assertThatEntityId(
@NonNull ID id,
@NonNull Class<M> modelClass
) {
return new WriteOperationIdAsserter<>(getEntityManager(), List.of(id), modelClass);
}

@SuppressWarnings("unused")
public static <ID extends EntityId<?>, M extends Identified<ID>> WriteOperationIdAsserter<ID, M> assertThatEntityId(
@NonNull Collection<ID> id,
@NonNull Class<M> modelClass
) {
return new WriteOperationIdAsserter<>(getEntityManager(), id, modelClass);
}

/**
* Batch query entities by their IDs using JPQL
*/
private static <ID extends EntityId<?>, M extends Identified<ID>> List<M> batchFindByIds(
EntityManager entityManager,
Collection<ID> ids,
Class<M> 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 <ID extends EntityId<?>, M extends Identified<ID>> Map<ID, Boolean> batchCheckExistence(
EntityManager entityManager,
Collection<ID> ids,
Class<M> 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<ID extends EntityId<?>, E extends Identified<ID> & ChangeAware, M extends Identified<ID> & ChangeAware> {
private final EntityManager entityManager;
private final E entity;
private final Collection<E> entity;
private final Class<M> 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<ID extends EntityId<?>, M extends Identified<ID>> {
private final EntityManager entityManager;
private final ID id;
private final Collection<ID> id;
private final Class<M> 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();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<ITEM> {
public ITEM returnFirst() {
return (ITEM) this.returnAll().getFirst();
}

public List<ITEM> returnAll() {
return this.fetch();
}

@SafeVarargs
public final List<ITEM> returnSorted(@NonNull Comparator<ITEM>... 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 <U extends Comparable<? super U>> List<ITEM> returnSorted(@NonNull Function<ITEM, ? extends Comparable<?>>... comparators) {
if (comparators.length == 0) {
throw new IllegalArgumentException("At least one comparator must be provided");
}

var combinedComparator = Comparator.comparing((Function<ITEM, U>) comparators[0]);
for (var i = 1; i < comparators.length; i++) {
combinedComparator = combinedComparator.thenComparing((Function<ITEM, U>) comparators[i]);
}

return returnAll().stream().sorted(combinedComparator).toList();
}

public <U extends Comparable<? super U>> AllAndFiltered<ITEM> returnFiltered(@NonNull Predicate<ITEM> predicate) {
var all = this.returnAll();
return new AllAndFiltered<ITEM>(
all,
all.stream().filter(predicate).toList(),
all.stream().filter(item -> !predicate.test(item)).toList()
);
}

public Set<ITEM> returnSet() {
return new HashSet<>(this.returnAll());
}

protected abstract List<ITEM> fetch();

public record AllAndFiltered<T>(@NonNull List<T> all, @NonNull List<T> filtered, @NonNull List<T> other) {

}
}
Loading