From b10451203b16bfbc0242a7396e4ed39f41f7c333 Mon Sep 17 00:00:00 2001 From: JesseBrault0709 <62299747+JesseBrault0709@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:06:41 +0200 Subject: [PATCH] Basic Recipe entity and models. --- build.gradle | 8 + .../app/mealsmadeeasy/api/recipe/Recipe.java | 21 ++ .../api/recipe/RecipeEntity.java | 170 +++++++++++++ .../api/recipe/RecipeException.java | 25 ++ .../api/recipe/RecipeRepository.java | 16 ++ .../api/recipe/RecipeService.java | 45 ++++ .../api/recipe/RecipeServiceImpl.java | 236 ++++++++++++++++++ .../api/recipe/comment/RecipeComment.java | 14 ++ .../recipe/comment/RecipeCommentEntity.java | 99 ++++++++ .../comment/RecipeCommentRepository.java | 8 + .../api/recipe/star/RecipeStar.java | 12 + .../api/recipe/star/RecipeStarEntity.java | 63 +++++ .../api/recipe/star/RecipeStarRepository.java | 14 ++ .../mealsmadeeasy/api/user/UserEntity.java | 39 +++ .../api/recipe/RecipeRepositoryTests.java | 102 ++++++++ .../api/recipe/RecipeServiceTests.java | 55 ++++ src/test/resources/application.properties | 6 + 17 files changed, 933 insertions(+) create mode 100644 src/main/java/app/mealsmadeeasy/api/recipe/Recipe.java create mode 100644 src/main/java/app/mealsmadeeasy/api/recipe/RecipeEntity.java create mode 100644 src/main/java/app/mealsmadeeasy/api/recipe/RecipeException.java create mode 100644 src/main/java/app/mealsmadeeasy/api/recipe/RecipeRepository.java create mode 100644 src/main/java/app/mealsmadeeasy/api/recipe/RecipeService.java create mode 100644 src/main/java/app/mealsmadeeasy/api/recipe/RecipeServiceImpl.java create mode 100644 src/main/java/app/mealsmadeeasy/api/recipe/comment/RecipeComment.java create mode 100644 src/main/java/app/mealsmadeeasy/api/recipe/comment/RecipeCommentEntity.java create mode 100644 src/main/java/app/mealsmadeeasy/api/recipe/comment/RecipeCommentRepository.java create mode 100644 src/main/java/app/mealsmadeeasy/api/recipe/star/RecipeStar.java create mode 100644 src/main/java/app/mealsmadeeasy/api/recipe/star/RecipeStarEntity.java create mode 100644 src/main/java/app/mealsmadeeasy/api/recipe/star/RecipeStarRepository.java create mode 100644 src/test/java/app/mealsmadeeasy/api/recipe/RecipeRepositoryTests.java create mode 100644 src/test/java/app/mealsmadeeasy/api/recipe/RecipeServiceTests.java create mode 100644 src/test/resources/application.properties diff --git a/build.gradle b/build.gradle index b0de051..30bce49 100644 --- a/build.gradle +++ b/build.gradle @@ -31,6 +31,14 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-api:0.12.5' implementation 'io.jsonwebtoken:jjwt-jackson:0.12.5' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5' + + implementation 'org.commonmark:commonmark:0.22.0' + implementation 'org.jsoup:jsoup:1.17.2' + + compileOnly 'org.jetbrains:annotations:24.1.0' + + // Custom testing + testRuntimeOnly 'com.h2database:h2' } tasks.named('test') { diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/Recipe.java b/src/main/java/app/mealsmadeeasy/api/recipe/Recipe.java new file mode 100644 index 0000000..6de85f8 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/recipe/Recipe.java @@ -0,0 +1,21 @@ +package app.mealsmadeeasy.api.recipe; + +import app.mealsmadeeasy.api.recipe.comment.RecipeComment; +import app.mealsmadeeasy.api.user.User; +import org.jetbrains.annotations.Nullable; + +import java.time.LocalDateTime; +import java.util.Set; + +public interface Recipe { + Long getId(); + LocalDateTime getCreated(); + @Nullable LocalDateTime getModified(); + String getTitle(); + String getRawText(); + User getOwner(); + Set getStarGazers(); + boolean isPublic(); + Set getViewers(); + Set getComments(); +} diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeEntity.java b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeEntity.java new file mode 100644 index 0000000..2b3f0c2 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeEntity.java @@ -0,0 +1,170 @@ +package app.mealsmadeeasy.api.recipe; + +import app.mealsmadeeasy.api.recipe.comment.RecipeComment; +import app.mealsmadeeasy.api.recipe.comment.RecipeCommentEntity; +import app.mealsmadeeasy.api.user.User; +import app.mealsmadeeasy.api.user.UserEntity; +import jakarta.persistence.*; +import org.jetbrains.annotations.Nullable; + +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Set; + +@Entity(name = "Recipe") +public final class RecipeEntity implements Recipe { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(nullable = false, updatable = false) + private Long id; + + @Column(nullable = false) + private LocalDateTime created = LocalDateTime.now(); + + private LocalDateTime modified; + + @Column(nullable = false) + private String title; + + @Column(nullable = false) + @Lob + @Basic(fetch = FetchType.LAZY) + private String rawText; + + @Lob + @Basic(fetch = FetchType.LAZY) + private String cachedRenderedText; + + @ManyToOne(optional = false) + @JoinColumn(name = "owner_id", nullable = false) + private UserEntity owner; + + @OneToMany + private Set starGazers = new HashSet<>(); + + @OneToMany(mappedBy = "recipe") + private Set comments = new HashSet<>(); + + @Column(nullable = false) + private Boolean isPublic = false; + + @ManyToMany + private Set viewers = new HashSet<>(); + + @Override + public Long getId() { + return this.id; + } + + public void setId(Long id) { + this.id = id; + } + + @Override + public LocalDateTime getCreated() { + return this.created; + } + + public void setCreated(LocalDateTime created) { + this.created = created; + } + + @Override + public @Nullable LocalDateTime getModified() { + return this.modified; + } + + public void setModified(@Nullable LocalDateTime modified) { + this.modified = modified; + } + + @Override + public String getTitle() { + return this.title; + } + + public void setTitle(String title) { + this.title = title; + } + + @Override + public String getRawText() { + return this.rawText; + } + + public void setRawText(String rawText) { + this.rawText = rawText; + } + + public String getCachedRenderedText() { + return this.cachedRenderedText; + } + + public void setCachedRenderedText(String cachedRenderedText) { + this.cachedRenderedText = cachedRenderedText; + } + + @Override + public UserEntity getOwner() { + return this.owner; + } + + public void setOwner(UserEntity owner) { + this.owner = owner; + } + + @Override + public boolean isPublic() { + return this.isPublic; + } + + public void setPublic(Boolean isPublic) { + this.isPublic = isPublic; + } + + @Override + public Set getViewers() { + return Set.copyOf(this.viewers); + } + + public Set getViewerEntities() { + return this.viewers; + } + + public void setViewers(Set viewers) { + this.viewers = viewers; + } + + @Override + public Set getStarGazers() { + return Set.copyOf(this.starGazers); + } + + public Set getStarGazerEntities() { + return this.starGazers; + } + + public void setStarGazers(Set starGazers) { + this.starGazers = starGazers; + } + + @Override + public Set getComments() { + return Set.copyOf(this.comments); + } + + public Set getCommentEntities() { + return this.comments; + } + + public void setComments(Set comments) { + this.comments = comments; + } + + @Override + public String toString() { + return "RecipeEntity(" + this.id + ", " + this.title + ")"; + } + +} diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeException.java b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeException.java new file mode 100644 index 0000000..82fdb2d --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeException.java @@ -0,0 +1,25 @@ +package app.mealsmadeeasy.api.recipe; + +public class RecipeException extends Exception { + + public enum Type { + INVALID_OWNER_USERNAME, INVALID_STAR, INVALID_ID + } + + private final Type type; + + public RecipeException(Type type, String message, Throwable cause) { + super(message, cause); + this.type = type; + } + + public RecipeException(Type type, String message) { + super(message); + this.type = type; + } + + public Type getType() { + return this.type; + } + +} diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeRepository.java b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeRepository.java new file mode 100644 index 0000000..b7bba04 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeRepository.java @@ -0,0 +1,16 @@ +package app.mealsmadeeasy.api.recipe; + +import app.mealsmadeeasy.api.user.UserEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; + +public interface RecipeRepository extends JpaRepository { + List findAllByIsPublicIsTrue(); + List findAllByViewersContaining(UserEntity viewer); + List findAllByOwner(UserEntity owner); + + @Query("SELECT r FROM Recipe r WHERE size(r.starGazers) > ?1") + List findAllByStarGazersGreaterThanEqual(long stars); +} diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeService.java b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeService.java new file mode 100644 index 0000000..f455b51 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeService.java @@ -0,0 +1,45 @@ +package app.mealsmadeeasy.api.recipe; + +import app.mealsmadeeasy.api.recipe.comment.RecipeComment; +import app.mealsmadeeasy.api.recipe.star.RecipeStar; +import app.mealsmadeeasy.api.user.User; + +import java.util.List; + +public interface RecipeService { + + Recipe create(String ownerUsername, String title, String rawText) throws RecipeException; + + Recipe getById(long id) throws RecipeException; + List getByMinimumStars(long minimumStars); + List getPublicRecipes(); + List getRecipesViewableBy(User user); + List getRecipesOwnedBy(User user); + + String getRenderedMarkdown(Recipe recipe); + + Recipe updateRawText(Recipe recipe, String newRawText); + + Recipe updateOwner(Recipe recipe, String newOwnerUsername) throws RecipeException; + + RecipeStar addStar(Recipe recipe, User giver); + void deleteStarByUser(Recipe recipe, User giver) throws RecipeException; + void deleteStar(RecipeStar recipeStar); + + Recipe setPublic(Recipe recipe, boolean isPublic); + + Recipe addViewer(Recipe recipe, User user); + Recipe removeViewer(Recipe recipe, User user); + Recipe clearViewers(Recipe recipe); + + RecipeComment getCommentById(long id) throws RecipeException; + RecipeComment addComment(Recipe recipe, String rawCommentText, User commenter); + RecipeComment updateComment(RecipeComment comment, String newRawCommentText); + String getRenderedMarkdown(RecipeComment recipeComment); + void deleteComment(RecipeComment comment); + Recipe clearComments(Recipe recipe); + + void deleteRecipe(Recipe recipe); + void deleteById(long id); + +} diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeServiceImpl.java b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeServiceImpl.java new file mode 100644 index 0000000..89ecb24 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeServiceImpl.java @@ -0,0 +1,236 @@ +package app.mealsmadeeasy.api.recipe; + +import app.mealsmadeeasy.api.recipe.comment.RecipeComment; +import app.mealsmadeeasy.api.recipe.comment.RecipeCommentEntity; +import app.mealsmadeeasy.api.recipe.comment.RecipeCommentRepository; +import app.mealsmadeeasy.api.recipe.star.RecipeStar; +import app.mealsmadeeasy.api.recipe.star.RecipeStarEntity; +import app.mealsmadeeasy.api.recipe.star.RecipeStarRepository; +import app.mealsmadeeasy.api.user.User; +import app.mealsmadeeasy.api.user.UserEntity; +import app.mealsmadeeasy.api.user.UserRepository; +import org.commonmark.parser.Parser; +import org.commonmark.renderer.html.HtmlRenderer; +import org.jsoup.Jsoup; +import org.jsoup.safety.Safelist; +import org.springframework.stereotype.Service; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Service +public final class RecipeServiceImpl implements RecipeService { + + private static String renderAndCleanMarkdown(String rawText) { + final var parser = Parser.builder().build(); + final var node = parser.parse(rawText); + final var htmlRenderer = HtmlRenderer.builder().build(); + final String unsafeHtml = htmlRenderer.render(node); + return Jsoup.clean(unsafeHtml, Safelist.relaxed()); + } + + private final RecipeRepository recipeRepository; + private final UserRepository userRepository; + private final RecipeStarRepository recipeStarRepository; + private final RecipeCommentRepository recipeCommentRepository; + + public RecipeServiceImpl( + RecipeRepository recipeRepository, + UserRepository userRepository, + RecipeStarRepository recipeStarRepository, + RecipeCommentRepository recipeCommentRepository + ) { + this.recipeRepository = recipeRepository; + this.userRepository = userRepository; + this.recipeStarRepository = recipeStarRepository; + this.recipeCommentRepository = recipeCommentRepository; + } + + @Override + public Recipe create(String ownerUsername, String title, String rawText) throws RecipeException { + final RecipeEntity draft = new RecipeEntity(); + final UserEntity userEntity = this.userRepository.findByUsername(ownerUsername) + .orElseThrow(() -> new RecipeException( + RecipeException.Type.INVALID_OWNER_USERNAME, + "No such ownerUsername " + ownerUsername + )); + draft.setOwner(userEntity); + draft.setTitle(title); + draft.setRawText(rawText); + return this.recipeRepository.save(draft); + } + + @Override + public Recipe getById(long id) throws RecipeException { + return this.recipeRepository.findById(id).orElseThrow(() -> new RecipeException( + RecipeException.Type.INVALID_ID, + "No such recipe for id " + id + )); + } + + @Override + public List getByMinimumStars(long minimumStars) { + return List.copyOf(this.recipeRepository.findAllByStarGazersGreaterThanEqual(minimumStars)); + } + + @Override + public List getPublicRecipes() { + return List.copyOf(this.recipeRepository.findAllByIsPublicIsTrue()); + } + + @Override + public List getRecipesViewableBy(User user) { + return List.copyOf(this.recipeRepository.findAllByViewersContaining((UserEntity) user)); + } + + @Override + public List getRecipesOwnedBy(User user) { + return List.copyOf(this.recipeRepository.findAllByOwner((UserEntity) user)); + } + + @Override + public String getRenderedMarkdown(Recipe recipe) { + RecipeEntity entity = (RecipeEntity) recipe; + if (entity.getCachedRenderedText() == null) { + entity.setCachedRenderedText(renderAndCleanMarkdown(entity.getRawText())); + entity = this.recipeRepository.save(entity); + } + return entity.getCachedRenderedText(); + } + + @Override + public Recipe updateRawText(Recipe recipe, String newRawText) { + final RecipeEntity entity = (RecipeEntity) recipe; + entity.setCachedRenderedText(null); + entity.setRawText(newRawText); + return this.recipeRepository.save(entity); + } + + @Override + public Recipe updateOwner(Recipe recipe, String newOwnerUsername) throws RecipeException { + final RecipeEntity entity = (RecipeEntity) recipe; + final UserEntity newOwner = this.userRepository.findByUsername(newOwnerUsername) + .orElseThrow(() -> new RecipeException( + RecipeException.Type.INVALID_OWNER_USERNAME, + "No such username: " + newOwnerUsername + )); + entity.setOwner(newOwner); + return this.recipeRepository.save(entity); + } + + @Override + public RecipeStar addStar(Recipe recipe, User giver) { + final RecipeEntity entity = (RecipeEntity) recipe; + final RecipeStarEntity star = new RecipeStarEntity(); + star.setOwner((UserEntity) giver); + star.setRecipe(entity); + return this.recipeStarRepository.save(star); + } + + @Override + public void deleteStar(RecipeStar recipeStar) { + this.recipeStarRepository.delete((RecipeStarEntity) recipeStar); + } + + @Override + public void deleteStarByUser(Recipe recipe, User giver) throws RecipeException { + final RecipeStarEntity star = this.recipeStarRepository.findByOwnerAndRecipe( + (UserEntity) giver, + (RecipeEntity) recipe + ).orElseThrow(() -> new RecipeException( + RecipeException.Type.INVALID_STAR, + "No such star for user " + giver.getUsername() + " and recipe " + recipe.getId() + )); + this.recipeStarRepository.delete(star); + } + + @Override + public Recipe setPublic(Recipe recipe, boolean isPublic) { + final RecipeEntity entity = (RecipeEntity) recipe; + entity.setPublic(isPublic); + return this.recipeRepository.save(entity); + } + + @Override + public Recipe addViewer(Recipe recipe, User user) { + final RecipeEntity entity = (RecipeEntity) recipe; + final Set viewers = new HashSet<>(entity.getViewerEntities()); + viewers.add((UserEntity) user); + entity.setViewers(viewers); + return this.recipeRepository.save(entity); + } + + @Override + public Recipe removeViewer(Recipe recipe, User user) { + final RecipeEntity entity = (RecipeEntity) recipe; + final Set viewers = new HashSet<>(entity.getViewerEntities()); + viewers.remove((UserEntity) user); + entity.setViewers(viewers); + return this.recipeRepository.save(entity); + } + + @Override + public Recipe clearViewers(Recipe recipe) { + final RecipeEntity entity = (RecipeEntity) recipe; + entity.setViewers(new HashSet<>()); + return this.recipeRepository.save(entity); + } + + @Override + public RecipeComment getCommentById(long id) throws RecipeException { + return this.recipeCommentRepository.findById(id) + .orElseThrow(() -> new RecipeException( + RecipeException.Type.INVALID_ID, + "No such RecipeComment for id " + id + )); + } + + @Override + public RecipeComment addComment(Recipe recipe, String rawCommentText, User commenter) { + final RecipeCommentEntity draft = new RecipeCommentEntity(); + draft.setRawText(rawCommentText); + draft.setOwner((UserEntity) commenter); + return this.recipeCommentRepository.save(draft); + } + + @Override + public RecipeComment updateComment(RecipeComment comment, String newRawCommentText) { + final RecipeCommentEntity entity = (RecipeCommentEntity) comment; + entity.setCachedRenderedText(null); + entity.setRawText(newRawCommentText); + return this.recipeCommentRepository.save(entity); + } + + @Override + public String getRenderedMarkdown(RecipeComment recipeComment) { + RecipeCommentEntity entity = (RecipeCommentEntity) recipeComment; + if (entity.getCachedRenderedText() == null) { + entity.setCachedRenderedText(renderAndCleanMarkdown(entity.getRawText())); + entity = this.recipeCommentRepository.save(entity); + } + return entity.getCachedRenderedText(); + } + + @Override + public void deleteComment(RecipeComment comment) { + this.recipeCommentRepository.delete((RecipeCommentEntity) comment); + } + + @Override + public Recipe clearComments(Recipe recipe) { + this.recipeCommentRepository.deleteAllByRecipe((RecipeEntity) recipe); + return this.recipeRepository.getReferenceById(recipe.getId()); + } + + @Override + public void deleteRecipe(Recipe recipe) { + this.recipeRepository.delete((RecipeEntity) recipe); + } + + @Override + public void deleteById(long id) { + this.recipeRepository.deleteById(id); + } + +} diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/comment/RecipeComment.java b/src/main/java/app/mealsmadeeasy/api/recipe/comment/RecipeComment.java new file mode 100644 index 0000000..eebaab7 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/recipe/comment/RecipeComment.java @@ -0,0 +1,14 @@ +package app.mealsmadeeasy.api.recipe.comment; + +import app.mealsmadeeasy.api.recipe.Recipe; +import app.mealsmadeeasy.api.user.User; + +import java.time.LocalDateTime; + +public interface RecipeComment { + LocalDateTime getCreated(); + LocalDateTime getModified(); + String getRawText(); + User getOwner(); + Recipe getRecipe(); +} diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/comment/RecipeCommentEntity.java b/src/main/java/app/mealsmadeeasy/api/recipe/comment/RecipeCommentEntity.java new file mode 100644 index 0000000..5c86dee --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/recipe/comment/RecipeCommentEntity.java @@ -0,0 +1,99 @@ +package app.mealsmadeeasy.api.recipe.comment; + +import app.mealsmadeeasy.api.recipe.RecipeEntity; +import app.mealsmadeeasy.api.user.UserEntity; +import jakarta.persistence.*; + +import java.time.LocalDateTime; + +@Entity(name = "RecipeComment") +public final class RecipeCommentEntity implements RecipeComment { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(nullable = false) + private Long id; + + @Column(nullable = false, updatable = false) + private LocalDateTime created = LocalDateTime.now(); + + private LocalDateTime modified; + + @Lob + @Basic(fetch = FetchType.LAZY) + private String rawText; + + @Lob + @Basic(fetch = FetchType.LAZY) + private String cachedRenderedText; + + @ManyToOne + @JoinColumn(name = "owner_id", nullable = false, updatable = false) + private UserEntity owner; + + @ManyToOne + @JoinColumn(name = "recipe_id", nullable = false, updatable = false) + private RecipeEntity recipe; + + public Long getId() { + return this.id; + } + + public void setId(Long id) { + this.id = id; + } + + @Override + public LocalDateTime getCreated() { + return this.created; + } + + public void setCreated(LocalDateTime created) { + this.created = created; + } + + @Override + public LocalDateTime getModified() { + return this.modified; + } + + public void setModified(LocalDateTime modified) { + this.modified = modified; + } + + @Override + public String getRawText() { + return this.rawText; + } + + public void setRawText(String rawText) { + this.rawText = rawText; + } + + public String getCachedRenderedText() { + return this.cachedRenderedText; + } + + public void setCachedRenderedText(String cachedRenderedText) { + this.cachedRenderedText = cachedRenderedText; + } + + @Override + public UserEntity getOwner() { + return this.owner; + } + + public void setOwner(UserEntity owner) { + this.owner = owner; + } + + @Override + public RecipeEntity getRecipe() { + return this.recipe; + } + + public void setRecipe(RecipeEntity recipe) { + this.recipe = recipe; + } + +} diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/comment/RecipeCommentRepository.java b/src/main/java/app/mealsmadeeasy/api/recipe/comment/RecipeCommentRepository.java new file mode 100644 index 0000000..452b22f --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/recipe/comment/RecipeCommentRepository.java @@ -0,0 +1,8 @@ +package app.mealsmadeeasy.api.recipe.comment; + +import app.mealsmadeeasy.api.recipe.RecipeEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RecipeCommentRepository extends JpaRepository { + void deleteAllByRecipe(RecipeEntity recipe); +} diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/star/RecipeStar.java b/src/main/java/app/mealsmadeeasy/api/recipe/star/RecipeStar.java new file mode 100644 index 0000000..0d57b6e --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/recipe/star/RecipeStar.java @@ -0,0 +1,12 @@ +package app.mealsmadeeasy.api.recipe.star; + +import app.mealsmadeeasy.api.recipe.Recipe; +import app.mealsmadeeasy.api.user.User; + +import java.time.LocalDateTime; + +public interface RecipeStar { + User getOwner(); + LocalDateTime getDate(); + Recipe getRecipe(); +} diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/star/RecipeStarEntity.java b/src/main/java/app/mealsmadeeasy/api/recipe/star/RecipeStarEntity.java new file mode 100644 index 0000000..6b1ee39 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/recipe/star/RecipeStarEntity.java @@ -0,0 +1,63 @@ +package app.mealsmadeeasy.api.recipe.star; + +import app.mealsmadeeasy.api.recipe.RecipeEntity; +import app.mealsmadeeasy.api.user.UserEntity; +import jakarta.persistence.*; + +import java.time.LocalDateTime; + +@Entity(name = "RecipeStar") +public final class RecipeStarEntity implements RecipeStar { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(nullable = false) + private Long id; + + @ManyToOne + @JoinColumn(name = "owner_id", nullable = false, updatable = false) + private UserEntity owner; + + @Column(nullable = false, updatable = false) + private LocalDateTime date = LocalDateTime.now(); + + @ManyToOne + @JoinColumn(name = "recipe_id", nullable = false, updatable = false) + private RecipeEntity recipe; + + public long getId() { + return this.id; + } + + public void setId(long id) { + this.id = id; + } + + @Override + public UserEntity getOwner() { + return this.owner; + } + + public void setOwner(UserEntity owner) { + this.owner = owner; + } + + @Override + public LocalDateTime getDate() { + return this.date; + } + + public void setDate(LocalDateTime date) { + this.date = date; + } + + @Override + public RecipeEntity getRecipe() { + return this.recipe; + } + + public void setRecipe(RecipeEntity recipe) { + this.recipe = recipe; + } + +} diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/star/RecipeStarRepository.java b/src/main/java/app/mealsmadeeasy/api/recipe/star/RecipeStarRepository.java new file mode 100644 index 0000000..515a659 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/recipe/star/RecipeStarRepository.java @@ -0,0 +1,14 @@ +package app.mealsmadeeasy.api.recipe.star; + +import app.mealsmadeeasy.api.recipe.RecipeEntity; +import app.mealsmadeeasy.api.user.UserEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface RecipeStarRepository extends JpaRepository { + List findAllByOwner(UserEntity user); + long countAllByOwner(UserEntity user); + Optional findByOwnerAndRecipe(UserEntity user, RecipeEntity recipe); +} diff --git a/src/main/java/app/mealsmadeeasy/api/user/UserEntity.java b/src/main/java/app/mealsmadeeasy/api/user/UserEntity.java index 475236f..c9e2d2d 100644 --- a/src/main/java/app/mealsmadeeasy/api/user/UserEntity.java +++ b/src/main/java/app/mealsmadeeasy/api/user/UserEntity.java @@ -5,9 +5,11 @@ import org.springframework.security.core.GrantedAuthority; import java.util.Collection; import java.util.HashSet; +import java.util.Objects; import java.util.Set; @Entity(name = "User") +@Table(name = "\"user\"") public final class UserEntity implements User { public static UserEntity getDefaultDraft() { @@ -141,4 +143,41 @@ public final class UserEntity implements User { this.enabled = enabled; } + @Override + public int hashCode() { + return Objects.hash( + this.id, + this.username, + this.email, + this.password, + this.authorities, + this.enabled, + this.expired, + this.locked, + this.credentialsExpired + ); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj instanceof User o) { + return Objects.equals(this.id, o.getId()) + && Objects.equals(this.username, o.getUsername()) + && Objects.equals(this.password, o.getPassword()) + && Objects.equals(this.authorities, o.getAuthorities()) + && Objects.equals(this.enabled, o.isEnabled()) + && Objects.equals(this.expired, !o.isAccountNonExpired()) + && Objects.equals(this.locked, !o.isAccountNonLocked()) + && Objects.equals(this.credentialsExpired, !o.isCredentialsNonExpired()); + } else { + return false; + } + } + + @Override + public String toString() { + return "User(" + this.id + ", " + this.username + ", " + this.email + ")"; + } + } diff --git a/src/test/java/app/mealsmadeeasy/api/recipe/RecipeRepositoryTests.java b/src/test/java/app/mealsmadeeasy/api/recipe/RecipeRepositoryTests.java new file mode 100644 index 0000000..0be0421 --- /dev/null +++ b/src/test/java/app/mealsmadeeasy/api/recipe/RecipeRepositoryTests.java @@ -0,0 +1,102 @@ +package app.mealsmadeeasy.api.recipe; + +import app.mealsmadeeasy.api.user.UserEntity; +import app.mealsmadeeasy.api.user.UserRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +public class RecipeRepositoryTests { + + @Autowired + private RecipeRepository recipeRepository; + + @Autowired + private UserRepository userRepository; + + private UserEntity getOwnerUser() { + final UserEntity recipeUser = UserEntity.getDefaultDraft(); + recipeUser.setUsername("recipeUser"); + recipeUser.setEmail("recipe@user.com"); + recipeUser.setPassword("test"); + return this.userRepository.save(recipeUser); + } + + private UserEntity getViewerUser() { + final UserEntity viewerUser = UserEntity.getDefaultDraft(); + viewerUser.setUsername("recipeViewerUser"); + viewerUser.setEmail("recipe-viewer@user.com"); + viewerUser.setPassword("test"); + return this.userRepository.save(viewerUser); + } + + @Test + @DirtiesContext + public void findsAllPublicRecipes() { + final RecipeEntity publicRecipe = new RecipeEntity(); + publicRecipe.setPublic(true); + publicRecipe.setOwner(this.getOwnerUser()); + publicRecipe.setTitle("Public Recipe"); + publicRecipe.setRawText("Hello, World!"); + this.recipeRepository.save(publicRecipe); + + final List publicRecipes = this.recipeRepository.findAllByIsPublicIsTrue(); + assertThat(publicRecipes.size()).isEqualTo(1); + } + + @Test + @DirtiesContext + public void doesNotFindNonPublicRecipe() { + final RecipeEntity nonPublicRecipe = new RecipeEntity(); + nonPublicRecipe.setOwner(this.getOwnerUser()); + nonPublicRecipe.setTitle("Non-Public Recipe"); + nonPublicRecipe.setRawText("Hello, World!"); + this.recipeRepository.save(nonPublicRecipe); + + final List publicRecipes = this.recipeRepository.findAllByIsPublicIsTrue(); + assertThat(publicRecipes.size()).isEqualTo(0); + } + + @Test + @DirtiesContext + public void findsAllForViewer() { + final RecipeEntity recipe = new RecipeEntity(); + recipe.setOwner(this.getOwnerUser()); + recipe.setTitle("Test Recipe"); + recipe.setRawText("Hello, World!"); + final RecipeEntity saved = this.recipeRepository.save(recipe); + + final UserEntity viewer = this.getViewerUser(); + final Set viewers = new HashSet<>(recipe.getViewerEntities()); + viewers.add(viewer); + saved.setViewers(viewers); + + this.recipeRepository.save(saved); + + final List viewable = this.recipeRepository.findAllByViewersContaining(viewer); + assertThat(viewable.size()).isEqualTo(1); + } + + @Test + @DirtiesContext + public void doesNotIncludeNonViewable() { + final RecipeEntity recipe = new RecipeEntity(); + recipe.setOwner(this.getOwnerUser()); + recipe.setTitle("Test Recipe"); + recipe.setRawText("Hello, World!"); + this.recipeRepository.save(recipe); + + final UserEntity viewer = this.getViewerUser(); + final List viewable = this.recipeRepository.findAllByViewersContaining(viewer); + assertThat(viewable.size()).isEqualTo(0); + } + +} diff --git a/src/test/java/app/mealsmadeeasy/api/recipe/RecipeServiceTests.java b/src/test/java/app/mealsmadeeasy/api/recipe/RecipeServiceTests.java new file mode 100644 index 0000000..d9868d8 --- /dev/null +++ b/src/test/java/app/mealsmadeeasy/api/recipe/RecipeServiceTests.java @@ -0,0 +1,55 @@ +package app.mealsmadeeasy.api.recipe; + +import app.mealsmadeeasy.api.user.User; +import app.mealsmadeeasy.api.user.UserEntity; +import app.mealsmadeeasy.api.user.UserRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +@SpringBootTest +public class RecipeServiceTests { + + @Autowired + private RecipeService recipeService; + + @Autowired + private UserRepository userRepository; + + private UserEntity createTestUser(String username) { + final UserEntity draft = UserEntity.getDefaultDraft(); + draft.setUsername(username); + draft.setEmail(username + "@test.com"); + draft.setPassword("test"); + return this.userRepository.save(draft); + } + + private Recipe createTestRecipe(User owner) throws RecipeException { + return this.recipeService.create(owner.getUsername(), "My Recipe" , "Hello!"); + } + + @Test + @DirtiesContext + public void simpleCreate() throws RecipeException { + final User user = this.createTestUser("recipeOwner"); + final Recipe recipe = this.recipeService.create(user.getUsername(), "My Recipe" , "Hello!"); + assertThat(recipe.getOwner(), is(user)); + assertThat(recipe.getTitle(), is("My Recipe")); + assertThat(recipe.getRawText(), is("Hello!")); + } + + @Test + @DirtiesContext + public void simpleGetById() throws RecipeException { + final Recipe testRecipe = this.createTestRecipe(this.createTestUser("recipeOwner")); + final Recipe byId = this.recipeService.getById(testRecipe.getId()); + assertThat(byId.getId(), is(testRecipe.getId())); + assertThat(byId.getTitle(), is("My Recipe")); + assertThat(byId.getRawText(), is("Hello!")); + } + +} diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 0000000..1a0c6f4 --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,6 @@ +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.security.access-token-lifetime=60 +app.mealsmadeeasy.api.security.refresh-token-lifetime=120