Refactoring of custom matchers and implementation/testing of ImageService.getImagesOwnedBy().

This commit is contained in:
Jesse Brault 2024-07-23 08:34:55 -05:00
parent 24db93111f
commit d4da12c349
14 changed files with 190 additions and 162 deletions

9
TODO.md Normal file
View File

@ -0,0 +1,9 @@
# TODO
* [ ] Tighten up api
* [ ] Follow conventions established in `S3ImageService` and then
`RecipeServiceImpl`.
* [ ] Tighten up things to be more Spring-like.
* [ ] Figure out Spring(-Boot) profiles for testing, staging, production, etc.
* [ ] Decide on how to have services running in production: containerized or
on-system installs?

View File

@ -16,8 +16,10 @@ import org.testcontainers.utility.DockerImageName;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import static app.mealsmadeeasy.api.matchers.Matchers.isUser;
import static app.mealsmadeeasy.api.image.ContainsImagesMatcher.containsImages;
import static app.mealsmadeeasy.api.user.IsUserMatcher.isUser;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
@ -59,6 +61,16 @@ public class S3ImageServiceTests {
}
}
private Image createHal9000(User owner, InputStream data) throws ImageException, IOException {
return this.imageService.create(
owner,
"HAL9000.svg",
data,
"image/svg+xml",
27881L
);
}
@Test
public void smokeScreen() {}
@ -67,13 +79,7 @@ public class S3ImageServiceTests {
public void simpleCreate() {
try (final InputStream hal9000 = getHal9000()) {
final User owner = this.createTestUser("imageOwner");
final Image image = this.imageService.create(
owner,
"HAL9000.svg",
hal9000,
"image/svg+xml",
27881L
);
final Image image = this.createHal9000(owner, hal9000);
assertThat(image.getOwner(), isUser(owner));
assertThat(image.getCreated(), is(notNullValue()));
assertThat(image.getModified(), is(nullValue()));
@ -94,13 +100,7 @@ public class S3ImageServiceTests {
public void loadImageWithOwner() {
try (final InputStream hal9000 = getHal9000()) {
final User owner = this.createTestUser("imageOwner");
final Image image = this.imageService.create(
owner,
"HAL9000.svg",
hal9000,
"image/svg+xml",
27881L
);
final Image image = this.createHal9000(owner, hal9000);
try (final InputStream stored = this.imageService.getImageContentById(image.getId(), owner)) {
final byte[] storedBytes = stored.readAllBytes();
assertThat(storedBytes.length, is(27881));
@ -114,13 +114,7 @@ public class S3ImageServiceTests {
public void loadPublicImage() {
try (final InputStream hal9000 = getHal9000()) {
final User owner = this.createTestUser("imageOwner");
Image image = this.imageService.create(
owner,
"HAL9000.svg",
hal9000,
"image/svg+xml",
27881L
);
Image image = this.createHal9000(owner, hal9000);
image = this.imageService.setPublic(image, owner, true);
try (final InputStream stored = this.imageService.getImageContentById(image.getId())) {
final byte[] storedBytes = stored.readAllBytes();
@ -137,13 +131,7 @@ public class S3ImageServiceTests {
try (final InputStream hal9000 = getHal9000()) {
final User owner = this.createTestUser("imageOwner");
final User viewer = this.createTestUser("imageViewer");
Image image = this.imageService.create(
owner,
"HAL9000.svg",
hal9000,
"image/svg+xml",
27881L
);
Image image = this.createHal9000(owner, hal9000);
image = this.imageService.addViewer(image, owner, viewer);
try (final InputStream stored = this.imageService.getImageContentById(image.getId(), viewer)) {
final byte[] storedBytes = stored.readAllBytes();
@ -154,4 +142,30 @@ public class S3ImageServiceTests {
}
}
@Test
@DirtiesContext
public void getImagesOwnedBy() {
try (
final InputStream hal9000_0 = getHal9000();
final InputStream hal9000_1 = getHal9000();
final InputStream hal9000_2 = getHal9000();
) {
final User owner = this.createTestUser("imageOwner");
final User otherOwner = this.createTestUser("otherImageOwner");
final Image image0 = this.createHal9000(owner, hal9000_0);
final Image image1 = this.createHal9000(owner, hal9000_1);
final Image image2 = this.createHal9000(otherOwner, hal9000_2);
final List<Image> ownedImages = this.imageService.getImagesOwnedBy(owner);
assertThat(ownedImages.size(), is(2));
assertThat(ownedImages, containsImages(image0, image1));
final List<Image> otherOwnedImages = this.imageService.getImagesOwnedBy(otherOwner);
assertThat(otherOwnedImages.size(), is(1));
assertThat(otherOwnedImages, containsImages(image2));
} catch (IOException | ImageException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -1,5 +1,6 @@
package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.user.IsUserMatcher;
import app.mealsmadeeasy.api.recipe.star.RecipeStar;
import app.mealsmadeeasy.api.user.User;
import app.mealsmadeeasy.api.user.UserEntity;
@ -12,7 +13,6 @@ import org.springframework.test.annotation.DirtiesContext;
import java.util.List;
import static app.mealsmadeeasy.api.matchers.Matchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
@ -115,7 +115,7 @@ public class RecipeServiceTests {
recipe = this.recipeService.setPublic(recipe, owner, true);
final RecipeStar star = this.recipeService.addStar(recipe, owner);
final Recipe byIdWithStars = this.recipeService.getByIdWithStars(recipe.getId());
assertThat(byIdWithStars.getStars(), containsStars(star));
assertThat(byIdWithStars.getStars(), ContainsRecipeStarsMatcher.containsStars(star));
}
@Test
@ -174,9 +174,9 @@ public class RecipeServiceTests {
assertThat(oneStar.size(), is(2));
assertThat(twoStars.size(), is(1));
assertThat(zeroStars, containsRecipes(r0, r1, r2));
assertThat(oneStar, containsRecipes(r1, r2));
assertThat(twoStars, containsRecipes(r2));
assertThat(zeroStars, ContainsRecipesMatcher.containsRecipes(r0, r1, r2));
assertThat(oneStar, ContainsRecipesMatcher.containsRecipes(r1, r2));
assertThat(twoStars, ContainsRecipesMatcher.containsRecipes(r2));
}
@Test
@ -223,9 +223,9 @@ public class RecipeServiceTests {
assertThat(oneStarViewable.size(), is(2));
assertThat(twoStarsViewable.size(), is (1));
assertThat(zeroStarsViewable, containsRecipes(r0, r1, r2));
assertThat(oneStarViewable, containsRecipes(r1, r2));
assertThat(twoStarsViewable, containsRecipes(r2));
assertThat(zeroStarsViewable, ContainsRecipesMatcher.containsRecipes(r0, r1, r2));
assertThat(oneStarViewable, ContainsRecipesMatcher.containsRecipes(r1, r2));
assertThat(twoStarsViewable, ContainsRecipesMatcher.containsRecipes(r2));
}
@Test
@ -241,7 +241,7 @@ public class RecipeServiceTests {
final List<Recipe> publicRecipes = this.recipeService.getPublicRecipes();
assertThat(publicRecipes.size(), is(2));
assertThat(publicRecipes, containsRecipes(r0, r1));
assertThat(publicRecipes, ContainsRecipesMatcher.containsRecipes(r0, r1));
}
@Test
@ -254,7 +254,7 @@ public class RecipeServiceTests {
r0 = this.recipeService.addViewer(r0, viewer);
final List<Recipe> viewableRecipes = this.recipeService.getRecipesViewableBy(viewer);
assertThat(viewableRecipes.size(), is(1));
assertThat(viewableRecipes, containsRecipes(r0));
assertThat(viewableRecipes, ContainsRecipesMatcher.containsRecipes(r0));
}
@Test
@ -264,7 +264,7 @@ public class RecipeServiceTests {
final Recipe r0 = this.createTestRecipe(owner);
final List<Recipe> ownedRecipes = this.recipeService.getRecipesOwnedBy(owner);
assertThat(ownedRecipes.size(), is(1));
assertThat(ownedRecipes, containsRecipes(r0));
assertThat(ownedRecipes, ContainsRecipesMatcher.containsRecipes(r0));
}
@Test
@ -318,7 +318,7 @@ public class RecipeServiceTests {
final User secondOwner = this.createTestUser("secondOwner");
Recipe recipe = this.createTestRecipe(firstOwner);
recipe = this.recipeService.updateOwner(recipe, firstOwner, secondOwner);
assertThat(recipe.getOwner(), isUser(secondOwner));
assertThat(recipe.getOwner(), IsUserMatcher.isUser(secondOwner));
}
@Test
@ -339,8 +339,8 @@ public class RecipeServiceTests {
Recipe recipe = this.createTestRecipe(owner);
recipe = this.recipeService.addViewer(recipe, starer);
final RecipeStar star = this.recipeService.addStar(recipe, starer);
assertThat(star.getRecipe(), isRecipe(recipe));
assertThat(star.getOwner(), isUser(starer));
assertThat(star.getRecipe(), IsRecipeMatcher.isRecipe(recipe));
assertThat(star.getOwner(), IsUserMatcher.isUser(starer));
}
@Test

View File

@ -1,13 +1,18 @@
package app.mealsmadeeasy.api.image;
import app.mealsmadeeasy.api.user.UserEntity;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
public interface ImageRepository extends JpaRepository<ImageEntity, Long> {
@Query("SELECT image FROM Image image WHERE image.id = ?1")
@EntityGraph(attributePaths = { "viewers" })
ImageEntity getByIdWithViewers(long id);
List<ImageEntity> findAllByOwner(UserEntity owner);
}

View File

@ -10,6 +10,7 @@ import org.springframework.stereotype.Service;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@ -89,7 +90,7 @@ public class S3ImageService implements ImageService {
@Override
public List<Image> getImagesOwnedBy(User user) {
return List.of();
return new ArrayList<>(this.imageRepository.findAllByOwner((UserEntity) user));
}
@Override

View File

@ -0,0 +1,17 @@
package app.mealsmadeeasy.api.image;
import app.mealsmadeeasy.api.matchers.ContainsItemsMatcher;
import java.util.List;
public final class ContainsImagesMatcher extends ContainsItemsMatcher<Image, Long> {
public static ContainsImagesMatcher containsImages(Image... expected) {
return new ContainsImagesMatcher(expected);
}
private ContainsImagesMatcher(Image... allExpected) {
super(List.of(allExpected), o -> o instanceof Image, Image::getId);
}
}

View File

@ -0,0 +1,54 @@
package app.mealsmadeeasy.api.matchers;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Predicate;
public class ContainsItemsMatcher<T, ID> extends BaseMatcher<Iterable<T>> {
private final List<T> allExpected;
private final Predicate<Object> isT;
private final Function<T, ID> idFunction;
public ContainsItemsMatcher(List<T> allExpected, Predicate<Object> isT, Function<T, ID> idFunction) {
this.allExpected = allExpected;
this.isT = isT;
this.idFunction = idFunction;
}
@SuppressWarnings("unchecked")
@Override
public boolean matches(Object o) {
if (o instanceof Iterable<?> iterable) {
checkExpected:
for (final T expected : this.allExpected) {
for (final Object item : iterable) {
if (
this.isT.test(item) && Objects.equals(
this.idFunction.apply((T) item),
this.idFunction.apply(expected)
)
) {
continue checkExpected;
}
}
// did not find expected in iterable
return false;
}
// found all expected in iterable
return true;
}
// o is not an iterable
return false;
}
@Override
public void describeTo(Description description) {
description.appendText("Expected ").appendValue(this.allExpected);
}
}

View File

@ -1,43 +0,0 @@
package app.mealsmadeeasy.api.matchers;
import app.mealsmadeeasy.api.recipe.Recipe;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import java.util.List;
import java.util.Objects;
public final class ContainsRecipesMatcher extends BaseMatcher<List<Recipe>> {
private final Recipe[] allExpected;
public ContainsRecipesMatcher(Recipe[] allExpected) {
this.allExpected = allExpected;
}
@Override
public boolean matches(Object actual) {
if (actual instanceof List<?> list) {
checkExpected:
for (final Recipe expected : allExpected) {
for (final Object item : list) {
if (item instanceof Recipe o && Objects.equals(o.getId(), expected.getId())) {
continue checkExpected;
}
}
// Did not find the expected in the list
return false;
}
// Found all expected in list
return true;
}
// actual is not a List
return false;
}
@Override
public void describeTo(Description description) {
description.appendText("Expected ").appendValue(List.of(this.allExpected));
}
}

View File

@ -1,43 +0,0 @@
package app.mealsmadeeasy.api.matchers;
import app.mealsmadeeasy.api.recipe.star.RecipeStar;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import java.util.Objects;
import java.util.Set;
public class ContainsStarsMatcher extends BaseMatcher<Set<RecipeStar>> {
private final RecipeStar[] allExpected;
public ContainsStarsMatcher(RecipeStar[] allExpected) {
this.allExpected = allExpected;
}
@Override
public boolean matches(Object actual) {
if (actual instanceof Set<?> set) {
checkExpected:
for (final RecipeStar expected : allExpected) {
for (final Object item : set) {
if (item instanceof RecipeStar o && Objects.equals(o.getId(), expected.getId())) {
continue checkExpected;
}
}
// Did not find the expected in the set
return false;
}
// Found all expected in set
return true;
}
// actual is not a Set
return false;
}
@Override
public void describeTo(Description description) {
description.appendText("Expected ").appendValue(Set.of(this.allExpected));
}
}

View File

@ -1,27 +0,0 @@
package app.mealsmadeeasy.api.matchers;
import app.mealsmadeeasy.api.recipe.Recipe;
import app.mealsmadeeasy.api.recipe.star.RecipeStar;
import app.mealsmadeeasy.api.user.User;
public final class Matchers {
public static ContainsRecipesMatcher containsRecipes(Recipe... expected) {
return new ContainsRecipesMatcher(expected);
}
public static ContainsStarsMatcher containsStars(RecipeStar... expected) {
return new ContainsStarsMatcher(expected);
}
public static IsRecipeMatcher isRecipe(Recipe expected) {
return new IsRecipeMatcher(expected);
}
public static IsUserMatcher isUser(User expected) {
return new IsUserMatcher(expected);
}
private Matchers() {}
}

View File

@ -0,0 +1,18 @@
package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.matchers.ContainsItemsMatcher;
import app.mealsmadeeasy.api.recipe.star.RecipeStar;
import java.util.List;
public class ContainsRecipeStarsMatcher extends ContainsItemsMatcher<RecipeStar, Long> {
public static ContainsRecipeStarsMatcher containsStars(RecipeStar... expected) {
return new ContainsRecipeStarsMatcher(expected);
}
private ContainsRecipeStarsMatcher(RecipeStar[] allExpected) {
super(List.of(allExpected), o -> o instanceof RecipeStar, RecipeStar::getId);
}
}

View File

@ -0,0 +1,17 @@
package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.matchers.ContainsItemsMatcher;
import java.util.List;
public final class ContainsRecipesMatcher extends ContainsItemsMatcher<Recipe, Long> {
public static ContainsRecipesMatcher containsRecipes(Recipe... expected) {
return new ContainsRecipesMatcher(expected);
}
private ContainsRecipesMatcher(Recipe[] allExpected) {
super(List.of(allExpected), o -> o instanceof Recipe, Recipe::getId);
}
}

View File

@ -1,6 +1,5 @@
package app.mealsmadeeasy.api.matchers;
package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.recipe.Recipe;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
@ -14,6 +13,10 @@ public class IsRecipeMatcher extends BaseMatcher<Recipe> {
this.expected = expected;
}
public static IsRecipeMatcher isRecipe(Recipe expected) {
return new IsRecipeMatcher(expected);
}
@Override
public boolean matches(Object actual) {
return actual instanceof Recipe o && Objects.equals(this.expected.getId(), o.getId());

View File

@ -1,6 +1,5 @@
package app.mealsmadeeasy.api.matchers;
package app.mealsmadeeasy.api.user;
import app.mealsmadeeasy.api.user.User;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
@ -14,6 +13,10 @@ public final class IsUserMatcher extends BaseMatcher<User> {
this.expected = expected;
}
public static IsUserMatcher isUser(User expected) {
return new IsUserMatcher(expected);
}
@Override
public boolean matches(Object actual) {
return actual instanceof User o && Objects.equals(o.getId(), this.expected.getId());