Recipe GET now working by ownerUsername and slug.
This commit is contained in:
parent
3d7d5d00f1
commit
57d2451be9
@ -39,6 +39,7 @@ public class RecipeControllerTests {
|
||||
|
||||
private Recipe createTestRecipe(User owner, boolean isPublic) {
|
||||
final RecipeCreateSpec spec = new RecipeCreateSpec();
|
||||
spec.setSlug("test-recipe");
|
||||
spec.setTitle("Test Recipe");
|
||||
spec.setRawText("# Hello, World!");
|
||||
spec.setPublic(isPublic);
|
||||
@ -50,12 +51,12 @@ public class RecipeControllerTests {
|
||||
public void getRecipePageViewByIdPublicRecipeNoPrincipal() throws Exception {
|
||||
final User owner = this.createTestUser("owner");
|
||||
final Recipe recipe = this.createTestRecipe(owner, true);
|
||||
this.mockMvc.perform(get("/recipes/{id}", recipe.getId()))
|
||||
this.mockMvc.perform(get("/recipes/{username}/{slug}", recipe.getOwner().getUsername(), recipe.getSlug()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.id").value(1))
|
||||
.andExpect(jsonPath("$.slug").value(recipe.getSlug()))
|
||||
.andExpect(jsonPath("$.title").value("Test Recipe"))
|
||||
.andExpect(jsonPath("$.text").value("<h1>Hello, World!</h1>"))
|
||||
.andExpect(jsonPath("$.ownerId").value(owner.getId()))
|
||||
.andExpect(jsonPath("$.ownerUsername").value(owner.getUsername()))
|
||||
.andExpect(jsonPath("$.starCount").value(0))
|
||||
.andExpect(jsonPath("$.viewerCount").value(0));
|
||||
@ -74,8 +75,8 @@ public class RecipeControllerTests {
|
||||
.andExpect(jsonPath("$.content", hasSize(1)))
|
||||
.andExpect(jsonPath("$.content[0].id").value(recipe.getId()))
|
||||
.andExpect(jsonPath("$.content[0].updated").exists())
|
||||
.andExpect(jsonPath("$.content[0].slug").value(recipe.getSlug()))
|
||||
.andExpect(jsonPath("$.content[0].title").value(recipe.getTitle()))
|
||||
.andExpect(jsonPath("$.content[0].ownerId").value(owner.getId()))
|
||||
.andExpect(jsonPath("$.content[0].ownerUsername").value(owner.getUsername()))
|
||||
.andExpect(jsonPath("$.content[0].public").value(true))
|
||||
.andExpect(jsonPath("$.content[0].starCount").value(0));
|
||||
|
@ -42,6 +42,7 @@ public class RecipeRepositoryTests {
|
||||
@DirtiesContext
|
||||
public void findsAllPublicRecipes() {
|
||||
final RecipeEntity publicRecipe = new RecipeEntity();
|
||||
publicRecipe.setSlug("public-recipe");
|
||||
publicRecipe.setPublic(true);
|
||||
publicRecipe.setOwner(this.getOwnerUser());
|
||||
publicRecipe.setTitle("Public Recipe");
|
||||
@ -56,6 +57,7 @@ public class RecipeRepositoryTests {
|
||||
@DirtiesContext
|
||||
public void doesNotFindNonPublicRecipe() {
|
||||
final RecipeEntity nonPublicRecipe = new RecipeEntity();
|
||||
nonPublicRecipe.setSlug("non-public-recipe");
|
||||
nonPublicRecipe.setOwner(this.getOwnerUser());
|
||||
nonPublicRecipe.setTitle("Non-Public Recipe");
|
||||
nonPublicRecipe.setRawText("Hello, World!");
|
||||
@ -69,6 +71,7 @@ public class RecipeRepositoryTests {
|
||||
@DirtiesContext
|
||||
public void findsAllForViewer() {
|
||||
final RecipeEntity recipe = new RecipeEntity();
|
||||
recipe.setSlug("test-recipe");
|
||||
recipe.setOwner(this.getOwnerUser());
|
||||
recipe.setTitle("Test Recipe");
|
||||
recipe.setRawText("Hello, World!");
|
||||
@ -89,6 +92,7 @@ public class RecipeRepositoryTests {
|
||||
@DirtiesContext
|
||||
public void doesNotIncludeNonViewable() {
|
||||
final RecipeEntity recipe = new RecipeEntity();
|
||||
recipe.setSlug("test-recipe");
|
||||
recipe.setOwner(this.getOwnerUser());
|
||||
recipe.setTitle("Test Recipe");
|
||||
recipe.setRawText("Hello, World!");
|
||||
|
@ -43,11 +43,16 @@ public class RecipeServiceTests {
|
||||
}
|
||||
|
||||
private Recipe createTestRecipe(@Nullable User owner) {
|
||||
return this.createTestRecipe(owner, false);
|
||||
return this.createTestRecipe(owner, false, null);
|
||||
}
|
||||
|
||||
private Recipe createTestRecipe(@Nullable User owner, boolean isPublic) {
|
||||
return this.createTestRecipe(owner, isPublic, null);
|
||||
}
|
||||
|
||||
private Recipe createTestRecipe(@Nullable User owner, boolean isPublic, @Nullable String slug) {
|
||||
final RecipeCreateSpec spec = new RecipeCreateSpec();
|
||||
spec.setSlug(slug != null ? slug : "my-recipe");
|
||||
spec.setTitle("My Recipe");
|
||||
spec.setRawText("Hello!");
|
||||
spec.setPublic(isPublic);
|
||||
@ -88,6 +93,7 @@ public class RecipeServiceTests {
|
||||
final Recipe recipe = this.createTestRecipe(owner, true);
|
||||
final Recipe byId = this.recipeService.getById(recipe.getId(), null);
|
||||
assertThat(byId.getId(), is(recipe.getId()));
|
||||
assertThat(byId.getSlug(), is(recipe.getSlug()));
|
||||
assertThat(byId.getTitle(), is("My Recipe"));
|
||||
assertThat(byId.getRawText(), is("Hello!"));
|
||||
assertThat(byId.isPublic(), is(true));
|
||||
@ -167,9 +173,9 @@ public class RecipeServiceTests {
|
||||
final User u0 = this.createTestUser("u0");
|
||||
final User u1 = this.createTestUser("u1");
|
||||
|
||||
final Recipe r0 = this.createTestRecipe(owner, true);
|
||||
final Recipe r1 = this.createTestRecipe(owner, true);
|
||||
final Recipe r2 = this.createTestRecipe(owner, true);
|
||||
final Recipe r0 = this.createTestRecipe(owner, true, "r0");
|
||||
final Recipe r1 = this.createTestRecipe(owner, true, "r1");
|
||||
final Recipe r2 = this.createTestRecipe(owner, true, "r2");
|
||||
|
||||
// r0.stars = 0, r1.stars = 1, r2.stars = 2
|
||||
this.recipeStarService.create(r1.getId(), u0.getUsername());
|
||||
@ -197,9 +203,9 @@ public class RecipeServiceTests {
|
||||
final User u1 = this.createTestUser("u1");
|
||||
final User viewer = this.createTestUser("recipeViewer");
|
||||
|
||||
Recipe r0 = this.createTestRecipe(owner); // not public
|
||||
Recipe r1 = this.createTestRecipe(owner);
|
||||
Recipe r2 = this.createTestRecipe(owner);
|
||||
Recipe r0 = this.createTestRecipe(owner, false, "r0"); // not public
|
||||
Recipe r1 = this.createTestRecipe(owner, false, "r1");
|
||||
Recipe r2 = this.createTestRecipe(owner, false, "r2");
|
||||
|
||||
for (final User starer : List.of(u0, u1)) {
|
||||
r0 = this.recipeService.addViewer(r0.getId(), owner, starer);
|
||||
@ -243,8 +249,8 @@ public class RecipeServiceTests {
|
||||
public void getPublicRecipes() {
|
||||
final User owner = this.createTestUser("recipeOwner");
|
||||
|
||||
Recipe r0 = this.createTestRecipe(owner, true);
|
||||
Recipe r1 = this.createTestRecipe(owner, true);
|
||||
Recipe r0 = this.createTestRecipe(owner, true, "r0");
|
||||
Recipe r1 = this.createTestRecipe(owner, true, "r1");
|
||||
|
||||
final List<Recipe> publicRecipes = this.recipeService.getPublicRecipes();
|
||||
assertThat(publicRecipes.size(), is(2));
|
||||
@ -279,6 +285,7 @@ public class RecipeServiceTests {
|
||||
public void updateRawText() throws RecipeException {
|
||||
final User owner = this.createTestUser("recipeOwner");
|
||||
final RecipeCreateSpec createSpec = new RecipeCreateSpec();
|
||||
createSpec.setSlug("my-recipe");
|
||||
createSpec.setTitle("My Recipe");
|
||||
createSpec.setRawText("# A Heading");
|
||||
Recipe recipe = this.recipeService.create(owner, createSpec);
|
||||
|
@ -2,6 +2,7 @@ spring.datasource.driver-class-name=org.h2.Driver
|
||||
spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1
|
||||
spring.datasource.username=sa
|
||||
spring.datasource.password=sa
|
||||
app.mealsmadeeasy.api.baseUrl=http://localhost:8080
|
||||
app.mealsmadeeasy.api.security.access-token-lifetime=60
|
||||
app.mealsmadeeasy.api.security.refresh-token-lifetime=120
|
||||
app.mealsmadeeasy.api.minio.endpoint=http://localhost:9000
|
||||
|
@ -61,6 +61,7 @@ public class DevConfiguration {
|
||||
logger.info("Created {}", obazdaImage);
|
||||
|
||||
final RecipeCreateSpec recipeCreateSpec = new RecipeCreateSpec();
|
||||
recipeCreateSpec.setSlug("test-recipe");
|
||||
recipeCreateSpec.setTitle("Test Recipe");
|
||||
recipeCreateSpec.setRawText("Hello, World!");
|
||||
recipeCreateSpec.setPublic(true);
|
||||
|
@ -13,6 +13,7 @@ public interface Recipe {
|
||||
Long getId();
|
||||
LocalDateTime getCreated();
|
||||
@Nullable LocalDateTime getModified();
|
||||
String getSlug();
|
||||
String getTitle();
|
||||
String getRawText();
|
||||
User getOwner();
|
||||
|
@ -31,10 +31,14 @@ public class RecipeController {
|
||||
));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<FullRecipeView> getById(@PathVariable long id, @AuthenticationPrincipal User user)
|
||||
@GetMapping("/{username}/{slug}")
|
||||
public ResponseEntity<FullRecipeView> getById(
|
||||
@PathVariable String username,
|
||||
@PathVariable String slug,
|
||||
@AuthenticationPrincipal User viewer
|
||||
)
|
||||
throws RecipeException {
|
||||
return ResponseEntity.ok(this.recipeService.getFullViewById(id, user));
|
||||
return ResponseEntity.ok(this.recipeService.getFullViewByUsernameAndSlug(username, slug, viewer));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
|
@ -27,6 +27,9 @@ public final class RecipeEntity implements Recipe {
|
||||
|
||||
private LocalDateTime modified;
|
||||
|
||||
@Column(nullable = false, unique = true)
|
||||
private String slug;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String title;
|
||||
|
||||
@ -86,6 +89,15 @@ public final class RecipeEntity implements Recipe {
|
||||
this.modified = modified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSlug() {
|
||||
return this.slug;
|
||||
}
|
||||
|
||||
public void setSlug(String slug) {
|
||||
this.slug = slug;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return this.title;
|
||||
|
@ -3,7 +3,7 @@ package app.mealsmadeeasy.api.recipe;
|
||||
public class RecipeException extends Exception {
|
||||
|
||||
public enum Type {
|
||||
INVALID_OWNER_USERNAME, INVALID_STAR, NOT_VIEWABLE, INVALID_COMMENT_ID, INVALID_ID
|
||||
INVALID_OWNER_USERNAME, INVALID_STAR, NOT_VIEWABLE, INVALID_COMMENT_ID, INVALID_USERNAME_OR_SLUG, INVALID_ID
|
||||
}
|
||||
|
||||
private final Type type;
|
||||
|
@ -18,6 +18,9 @@ public interface RecipeRepository extends JpaRepository<RecipeEntity, Long> {
|
||||
|
||||
List<RecipeEntity> findAllByOwner(UserEntity owner);
|
||||
|
||||
@Query("SELECT r from Recipe r WHERE r.owner.username = ?1 AND r.slug = ?2")
|
||||
Optional<RecipeEntity> findByOwnerUsernameAndSlug(String ownerUsername, String slug);
|
||||
|
||||
@Query("SELECT r FROM Recipe r WHERE size(r.stars) >= ?1 AND (r.isPublic OR ?2 MEMBER OF r.viewers)")
|
||||
List<RecipeEntity> findAllViewableByStarsGreaterThanEqual(long stars, UserEntity viewer);
|
||||
|
||||
|
@ -7,5 +7,6 @@ public interface RecipeSecurity {
|
||||
boolean isOwner(Recipe recipe, User user);
|
||||
boolean isOwner(long recipeId, User user) throws RecipeException;
|
||||
boolean isViewableBy(Recipe recipe, @Nullable User user) throws RecipeException;
|
||||
boolean isViewableBy(String ownerUsername, String slug, @Nullable User user) throws RecipeException;
|
||||
boolean isViewableBy(long recipeId, @Nullable User user) throws RecipeException;
|
||||
}
|
||||
|
@ -56,6 +56,16 @@ public class RecipeSecurityImpl implements RecipeSecurity {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isViewableBy(String ownerUsername, String slug, @Nullable User user) throws RecipeException {
|
||||
final Recipe recipe = this.recipeRepository.findByOwnerUsernameAndSlug(ownerUsername, slug)
|
||||
.orElseThrow(() -> new RecipeException(
|
||||
RecipeException.Type.INVALID_USERNAME_OR_SLUG,
|
||||
"No such Recipe for username " + ownerUsername + " and slug: " + slug
|
||||
));
|
||||
return this.isViewableBy(recipe, user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isViewableBy(long recipeId, @Nullable User user) throws RecipeException {
|
||||
final Recipe recipe = this.recipeRepository.findById(recipeId).orElseThrow(() -> new RecipeException(
|
||||
|
@ -18,6 +18,7 @@ public interface RecipeService {
|
||||
Recipe getById(long id, @Nullable User viewer) throws RecipeException;
|
||||
Recipe getByIdWithStars(long id, @Nullable User viewer) throws RecipeException;
|
||||
FullRecipeView getFullViewById(long id, @Nullable User viewer) throws RecipeException;
|
||||
FullRecipeView getFullViewByUsernameAndSlug(String username, String slug, @Nullable User viewer) throws RecipeException;
|
||||
|
||||
Slice<RecipeInfoView> getInfoViewsViewableBy(Pageable pageable, @Nullable User viewer);
|
||||
List<Recipe> getByMinimumStars(long minimumStars, @Nullable User viewer);
|
||||
|
@ -53,6 +53,7 @@ public class RecipeServiceImpl implements RecipeService {
|
||||
final RecipeEntity draft = new RecipeEntity();
|
||||
draft.setCreated(LocalDateTime.now());
|
||||
draft.setOwner((UserEntity) owner);
|
||||
draft.setSlug(spec.getSlug());
|
||||
draft.setTitle(spec.getTitle());
|
||||
draft.setRawText(spec.getRawText());
|
||||
draft.setMainImage((S3ImageEntity) spec.getMainImage());
|
||||
@ -97,26 +98,44 @@ public class RecipeServiceImpl implements RecipeService {
|
||||
return this.recipeRepository.getViewerCount(recipeId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@PostAuthorize("@recipeSecurity.isViewableBy(#id, #viewer)")
|
||||
public FullRecipeView getFullViewById(long id, @Nullable User viewer) throws RecipeException {
|
||||
final RecipeEntity recipe = this.recipeRepository.findById(id).orElseThrow(() -> new RecipeException(
|
||||
RecipeException.Type.INVALID_ID, "No such Recipe for id: " + id
|
||||
));
|
||||
private FullRecipeView getFullView(RecipeEntity recipe) {
|
||||
final FullRecipeView view = new FullRecipeView();
|
||||
view.setId(recipe.getId());
|
||||
view.setCreated(recipe.getCreated());
|
||||
view.setModified(recipe.getModified());
|
||||
view.setSlug(recipe.getSlug());
|
||||
view.setTitle(recipe.getTitle());
|
||||
view.setText(this.getRenderedMarkdown(recipe));
|
||||
view.setOwnerId(recipe.getOwner().getId());
|
||||
view.setOwnerUsername(recipe.getOwner().getUsername());
|
||||
view.setStarCount(this.getStarCount(recipe));
|
||||
view.setViewerCount(this.getViewerCount(recipe.getId()));
|
||||
if (recipe.getMainImage() != null) {
|
||||
view.setMainImage(this.imageService.toImageView(recipe.getMainImage()));
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
@PreAuthorize("@recipeSecurity.isViewableBy(#id, #viewer)")
|
||||
public FullRecipeView getFullViewById(long id, @Nullable User viewer) throws RecipeException {
|
||||
final RecipeEntity recipe = this.recipeRepository.findById(id).orElseThrow(() -> new RecipeException(
|
||||
RecipeException.Type.INVALID_ID, "No such Recipe for id: " + id
|
||||
));
|
||||
return this.getFullView(recipe);
|
||||
}
|
||||
|
||||
@Override
|
||||
@PreAuthorize("@recipeSecurity.isViewableBy(#username, #slug, #viewer)")
|
||||
public FullRecipeView getFullViewByUsernameAndSlug(String username, String slug, @Nullable User viewer) throws RecipeException {
|
||||
final RecipeEntity recipe = this.recipeRepository.findByOwnerUsernameAndSlug(username, slug)
|
||||
.orElseThrow(() -> new RecipeException(
|
||||
RecipeException.Type.INVALID_USERNAME_OR_SLUG,
|
||||
"No such Recipe for username " + username + " and slug: " + slug
|
||||
));
|
||||
return this.getFullView(recipe);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Slice<RecipeInfoView> getInfoViewsViewableBy(Pageable pageable, @Nullable User viewer) {
|
||||
return this.recipeRepository.findAllViewableBy((UserEntity) viewer, pageable).map(entity -> {
|
||||
@ -127,12 +146,14 @@ public class RecipeServiceImpl implements RecipeService {
|
||||
} else {
|
||||
view.setUpdated(entity.getCreated());
|
||||
}
|
||||
view.setSlug(entity.getSlug());
|
||||
view.setTitle(entity.getTitle());
|
||||
view.setOwnerId(entity.getOwner().getId());
|
||||
view.setOwnerUsername(entity.getOwner().getUsername());
|
||||
view.setPublic(entity.isPublic());
|
||||
view.setStarCount(this.getStarCount(entity));
|
||||
if (entity.getMainImage() != null) {
|
||||
view.setMainImage(this.imageService.toImageView(entity.getMainImage()));
|
||||
}
|
||||
return view;
|
||||
});
|
||||
}
|
||||
@ -164,6 +185,10 @@ public class RecipeServiceImpl implements RecipeService {
|
||||
public Recipe update(long id, RecipeUpdateSpec spec, User modifier) throws RecipeException {
|
||||
final RecipeEntity entity = this.findRecipeEntity(id);
|
||||
boolean didModify = false;
|
||||
if (spec.getSlug() != null) {
|
||||
entity.setSlug(spec.getSlug());
|
||||
didModify = true;
|
||||
}
|
||||
if (spec.getTitle() != null) {
|
||||
entity.setTitle(spec.getTitle());
|
||||
didModify = true;
|
||||
|
@ -5,11 +5,20 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class RecipeCreateSpec {
|
||||
|
||||
private String slug;
|
||||
private String title;
|
||||
private String rawText;
|
||||
private boolean isPublic;
|
||||
private @Nullable Image mainImage;
|
||||
|
||||
public String getSlug() {
|
||||
return this.slug;
|
||||
}
|
||||
|
||||
public void setSlug(String slug) {
|
||||
this.slug = slug;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return this.title;
|
||||
}
|
||||
|
@ -5,11 +5,20 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class RecipeUpdateSpec {
|
||||
|
||||
private @Nullable String slug;
|
||||
private @Nullable String title;
|
||||
private @Nullable String rawText;
|
||||
private @Nullable Boolean isPublic;
|
||||
private @Nullable Image mainImage;
|
||||
|
||||
public @Nullable String getSlug() {
|
||||
return this.slug;
|
||||
}
|
||||
|
||||
public void setSlug(@Nullable String slug) {
|
||||
this.slug = slug;
|
||||
}
|
||||
|
||||
public @Nullable String getTitle() {
|
||||
return this.title;
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ public class FullRecipeView {
|
||||
private long id;
|
||||
private LocalDateTime created;
|
||||
private LocalDateTime modified;
|
||||
private String slug;
|
||||
private String title;
|
||||
private String text;
|
||||
private long ownerId;
|
||||
@ -42,6 +43,14 @@ public class FullRecipeView {
|
||||
this.modified = modified;
|
||||
}
|
||||
|
||||
public String getSlug() {
|
||||
return this.slug;
|
||||
}
|
||||
|
||||
public void setSlug(String slug) {
|
||||
this.slug = slug;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return this.title;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package app.mealsmadeeasy.api.recipe.view;
|
||||
|
||||
import app.mealsmadeeasy.api.image.view.ImageView;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@ -8,12 +9,12 @@ public final class RecipeInfoView {
|
||||
|
||||
private long id;
|
||||
private LocalDateTime updated;
|
||||
private String slug;
|
||||
private String title;
|
||||
private long ownerId;
|
||||
private String ownerUsername;
|
||||
private boolean isPublic;
|
||||
private int starCount;
|
||||
private ImageView mainImage;
|
||||
private @Nullable ImageView mainImage;
|
||||
|
||||
public long getId() {
|
||||
return this.id;
|
||||
@ -31,6 +32,14 @@ public final class RecipeInfoView {
|
||||
this.updated = updated;
|
||||
}
|
||||
|
||||
public String getSlug() {
|
||||
return this.slug;
|
||||
}
|
||||
|
||||
public void setSlug(String slug) {
|
||||
this.slug = slug;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return this.title;
|
||||
}
|
||||
@ -39,14 +48,6 @@ public final class RecipeInfoView {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public long getOwnerId() {
|
||||
return this.ownerId;
|
||||
}
|
||||
|
||||
public void setOwnerId(long ownerId) {
|
||||
this.ownerId = ownerId;
|
||||
}
|
||||
|
||||
public String getOwnerUsername() {
|
||||
return this.ownerUsername;
|
||||
}
|
||||
@ -71,11 +72,11 @@ public final class RecipeInfoView {
|
||||
this.starCount = starCount;
|
||||
}
|
||||
|
||||
public ImageView getMainImage() {
|
||||
public @Nullable ImageView getMainImage() {
|
||||
return this.mainImage;
|
||||
}
|
||||
|
||||
public void setMainImage(ImageView mainImage) {
|
||||
public void setMainImage(@Nullable ImageView mainImage) {
|
||||
this.mainImage = mainImage;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user