MME-34 Add recipe star toggle endpoint, deprecate old star endpoints and service methods.

This commit is contained in:
Jesse Brault 2026-02-21 16:43:33 -06:00
parent 20865a1b5a
commit a73dcd1c01
4 changed files with 71 additions and 43 deletions

View File

@ -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<RecipeStar> 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<RecipeStar> maybeRecipeStar = this.recipeStarService.toggle(recipe, user); // get rid
assertThat(maybeRecipeStar.isEmpty(), is(true));
}
}

View File

@ -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<RecipeStar> 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<Object> toggleStar(
@PathVariable String username,
@PathVariable String slug,
@AuthenticationPrincipal User principal
) {
final Optional<RecipeStar> 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<Map<String, Object>> getStar(
@PathVariable String username,
@ -164,6 +184,7 @@ public class RecipesController {
}
}
@Deprecated
@DeleteMapping("/{username}/{slug}/star")
public ResponseEntity<Object> removeStar(
@PathVariable String username,

View File

@ -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<RecipeStar, Long> {
Optional<RecipeStar> findByRecipeIdAndOwnerId(Integer recipeId, Integer ownerId);
Optional<RecipeStar> 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);

View File

@ -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<RecipeStar> toggle(Recipe recipe, User starer) {
final Optional<RecipeStar> 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<RecipeStar> find(Recipe recipe, User starer) {
return this.recipeStarRepository.findByRecipeAndOwner(recipe, starer);
}
@Deprecated
@PreAuthorize("@recipeSecurity.isViewableBy(#recipeOwnerUsername, #recipeSlug, #starer)")
public Optional<RecipeStar> 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());