From a73dcd1c014d1ed8d2d099ad3621b915f31c31ee Mon Sep 17 00:00:00 2001 From: Jesse Brault Date: Sat, 21 Feb 2026 16:43:33 -0600 Subject: [PATCH] MME-34 Add recipe star toggle endpoint, deprecate old star endpoints and service methods. --- .../recipe/star/RecipeStarServiceTests.java | 68 +++++++------------ .../api/recipe/RecipesController.java | 21 ++++++ .../api/recipe/star/RecipeStarRepository.java | 3 + .../api/recipe/star/RecipeStarService.java | 22 ++++++ 4 files changed, 71 insertions(+), 43 deletions(-) diff --git a/src/integrationTest/java/app/mealsmadeeasy/api/recipe/star/RecipeStarServiceTests.java b/src/integrationTest/java/app/mealsmadeeasy/api/recipe/star/RecipeStarServiceTests.java index ad26860..d489d10 100644 --- a/src/integrationTest/java/app/mealsmadeeasy/api/recipe/star/RecipeStarServiceTests.java +++ b/src/integrationTest/java/app/mealsmadeeasy/api/recipe/star/RecipeStarServiceTests.java @@ -7,18 +7,16 @@ import app.mealsmadeeasy.api.recipe.spec.RecipeCreateSpec; import app.mealsmadeeasy.api.user.User; import app.mealsmadeeasy.api.user.UserCreateException; import app.mealsmadeeasy.api.user.UserService; -import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import java.util.Optional; import java.util.UUID; import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.*; @SpringBootTest @ExtendWith(PostgresTestsExtension.class) @@ -53,55 +51,39 @@ public class RecipeStarServiceTests { } @Test - public void createViaUsernameAndSlug() { - final User owner = this.seedUser(); - final User starer = this.seedUser(); - final Recipe recipe = this.seedRecipe(owner); - final RecipeStar star = assertDoesNotThrow(() -> this.recipeStarService.create( - recipe.getOwner().getUsername(), - recipe.getSlug(), - starer - )); - assertThat(star.getTimestamp(), is(notNullValue())); + public void whenToggleCreates_findReturnsPresent() { + final User user = this.seedUser(); + final Recipe recipe = this.seedRecipe(user); + this.recipeStarService.toggle(recipe, user); + assertThat(this.recipeStarService.find(recipe, user).isPresent(), is(true)); } @Test - public void createViaId() { - final User owner = this.seedUser(); - final User starer = this.seedUser(); - final Recipe recipe = this.seedRecipe(owner); - final RecipeStar star = assertDoesNotThrow(() -> this.recipeStarService.create( - recipe.getId(), - starer.getId() - )); - assertThat(star.getTimestamp(), is(notNullValue())); + public void whenToggleDeletes_findReturnsEmpty() { + final User user = this.seedUser(); + final Recipe recipe = this.seedRecipe(user); + this.recipeStarService.toggle(recipe, user); // create + assertThat(this.recipeStarService.find(recipe, user).isPresent(), is(true)); + this.recipeStarService.toggle(recipe, user); // delete + assertThat(this.recipeStarService.find(recipe, user).isEmpty(), is(true)); } @Test - public void find() { - final User owner = this.seedUser(); - final User starer = this.seedUser(); - final Recipe recipe = this.seedRecipe(owner); - this.recipeStarService.create(recipe.getId(), starer.getId()); - final @Nullable RecipeStar star = this.recipeStarService.find( - recipe.getOwner().getUsername(), - recipe.getSlug(), - starer - ).orElse(null); - assertThat(star, is(notNullValue())); + public void whenNotStarred_toggleReturnsPresent() { + final User user = this.seedUser(); + final Recipe recipe = this.seedRecipe(user); + final Optional maybeRecipeStar = this.recipeStarService.toggle(recipe, user); + assertThat(maybeRecipeStar.isPresent(), is(true)); } @Test - public void deleteViaUsernameAndSlug() { - final User owner = this.seedUser(); - final User starer = this.seedUser(); - final Recipe recipe = this.seedRecipe(owner); - this.recipeStarService.create(recipe.getId(), starer.getId()); - assertDoesNotThrow(() -> this.recipeStarService.delete( - recipe.getOwner().getUsername(), - recipe.getSlug(), - starer - )); + public void whenStarred_toggleReturnsEmpty() { + final User user = this.seedUser(); + final Recipe recipe = this.seedRecipe(user); + this.recipeStarService.toggle(recipe, user); // create + assertThat(this.recipeStarService.find(recipe, user).isPresent(), is(true)); // check that it's there + final Optional maybeRecipeStar = this.recipeStarService.toggle(recipe, user); // get rid + assertThat(maybeRecipeStar.isEmpty(), is(true)); } } diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/RecipesController.java b/src/main/java/app/mealsmadeeasy/api/recipe/RecipesController.java index bbec3e6..974473a 100644 --- a/src/main/java/app/mealsmadeeasy/api/recipe/RecipesController.java +++ b/src/main/java/app/mealsmadeeasy/api/recipe/RecipesController.java @@ -31,6 +31,7 @@ import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; @RestController @RequestMapping("/recipes") @@ -141,6 +142,7 @@ public class RecipesController { return ResponseEntity.noContent().build(); } + @Deprecated @PostMapping("/{username}/{slug}/star") public ResponseEntity addStar( @PathVariable String username, @@ -150,6 +152,24 @@ public class RecipesController { return ResponseEntity.status(HttpStatus.CREATED).body(this.recipeStarService.create(username, slug, principal)); } + @PostMapping("/{username}/{slug}/star/toggle") + public ResponseEntity toggleStar( + @PathVariable String username, + @PathVariable String slug, + @AuthenticationPrincipal User principal + ) { + final Optional maybeStar = this.recipeStarService.toggle( + this.recipeService.getByUsernameAndSlug(username, slug, principal), + principal + ); + if (maybeStar.isPresent()) { + return ResponseEntity.status(HttpStatus.CREATED).body(maybeStar.get()); + } else { + return ResponseEntity.noContent().build(); + } + } + + @Deprecated @GetMapping("/{username}/{slug}/star") public ResponseEntity> getStar( @PathVariable String username, @@ -164,6 +184,7 @@ public class RecipesController { } } + @Deprecated @DeleteMapping("/{username}/{slug}/star") public ResponseEntity removeStar( @PathVariable String username, diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/star/RecipeStarRepository.java b/src/main/java/app/mealsmadeeasy/api/recipe/star/RecipeStarRepository.java index 7fee276..183503b 100644 --- a/src/main/java/app/mealsmadeeasy/api/recipe/star/RecipeStarRepository.java +++ b/src/main/java/app/mealsmadeeasy/api/recipe/star/RecipeStarRepository.java @@ -1,5 +1,7 @@ package app.mealsmadeeasy.api.recipe.star; +import app.mealsmadeeasy.api.recipe.Recipe; +import app.mealsmadeeasy.api.user.User; import jakarta.transaction.Transactional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -9,6 +11,7 @@ import java.util.Optional; public interface RecipeStarRepository extends JpaRepository { Optional findByRecipeIdAndOwnerId(Integer recipeId, Integer ownerId); + Optional findByRecipeAndOwner(Recipe recipe, User owner); @Query("SELECT count(rs) > 0 FROM RecipeStar rs, Recipe r WHERE r.owner.username = ?1 AND r.slug = ?2 AND r.id = rs.recipe.id AND rs.owner.id = ?3") boolean isStarer(String ownerUsername, String slug, Integer viewerId); diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/star/RecipeStarService.java b/src/main/java/app/mealsmadeeasy/api/recipe/star/RecipeStarService.java index 297680c..cff65f6 100644 --- a/src/main/java/app/mealsmadeeasy/api/recipe/star/RecipeStarService.java +++ b/src/main/java/app/mealsmadeeasy/api/recipe/star/RecipeStarService.java @@ -7,6 +7,7 @@ import app.mealsmadeeasy.api.user.User; import app.mealsmadeeasy.api.user.UserRepository; import app.mealsmadeeasy.api.util.NoSuchEntityWithIdException; import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Service; import java.util.Optional; @@ -20,6 +21,7 @@ public class RecipeStarService { private final RecipeRepository recipeRepository; private final RecipeService recipeService; + @Deprecated public RecipeStar create(Recipe recipe, User owner) { final RecipeStar recipeStar = new RecipeStar(); recipeStar.setOwner(owner); @@ -51,15 +53,35 @@ public class RecipeStarService { return existing.orElseGet(() -> this.create(recipe, starer)); } + @PreAuthorize("@recipeSecurity.isViewableBy(#recipe, #starer)") + public Optional toggle(Recipe recipe, User starer) { + final Optional existing = this.find(recipe, starer); + if (existing.isPresent()) { + this.recipeStarRepository.delete(existing.get()); + return Optional.empty(); + } else { + return Optional.of(this.create(recipe, starer)); + } + } + + @PreAuthorize("@recipeSecurity.isViewableBy(#recipe, #starer)") + public Optional find(Recipe recipe, User starer) { + return this.recipeStarRepository.findByRecipeAndOwner(recipe, starer); + } + + @Deprecated + @PreAuthorize("@recipeSecurity.isViewableBy(#recipeOwnerUsername, #recipeSlug, #starer)") public Optional find(String recipeOwnerUsername, String recipeSlug, User starer) { final Recipe recipe = this.recipeService.getByUsernameAndSlug(recipeOwnerUsername, recipeSlug, starer); return this.recipeStarRepository.findByRecipeIdAndOwnerId(recipe.getId(), starer.getId()); } + @Deprecated public void delete(Integer recipeId, Integer ownerId) { this.recipeStarRepository.deleteByRecipeIdAndOwnerId(recipeId, ownerId); } + @Deprecated public void delete(String recipeOwnerUsername, String recipeSlug, User starer) { final Recipe recipe = this.recipeService.getByUsernameAndSlug(recipeOwnerUsername, recipeSlug, starer); this.delete(recipe.getId(), starer.getId());