diff --git a/src/main/java/com/example/solidconnection/board/service/BoardService.java b/src/main/java/com/example/solidconnection/board/service/BoardService.java index 3a74b919c..1ec5ac8b0 100644 --- a/src/main/java/com/example/solidconnection/board/service/BoardService.java +++ b/src/main/java/com/example/solidconnection/board/service/BoardService.java @@ -9,12 +9,15 @@ import com.example.solidconnection.type.BoardCode; import com.example.solidconnection.type.PostCategory; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.EnumUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.stream.Collectors; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_CATEGORY; + @Service @RequiredArgsConstructor public class BoardService { @@ -28,12 +31,11 @@ private String validateCode(String code) { } } - private PostCategory validatePostCategory(String postCategory) { - try { - return PostCategory.valueOf(postCategory); - } catch (IllegalArgumentException ex) { - throw new CustomException(ErrorCode.INVALID_POST_CATEGORY); + private PostCategory validatePostCategory(String category){ + if(!EnumUtils.isValidEnum(PostCategory.class, category)){ + throw new CustomException(INVALID_POST_CATEGORY); } + return PostCategory.valueOf(category); } @Transactional(readOnly = true) diff --git a/src/main/java/com/example/solidconnection/comment/controller/CommentController.java b/src/main/java/com/example/solidconnection/comment/controller/CommentController.java new file mode 100644 index 000000000..61bae1036 --- /dev/null +++ b/src/main/java/com/example/solidconnection/comment/controller/CommentController.java @@ -0,0 +1,62 @@ +package com.example.solidconnection.comment.controller; + +import com.example.solidconnection.comment.dto.*; +import com.example.solidconnection.comment.service.CommentService; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.security.Principal; + +import static com.example.solidconnection.config.swagger.SwaggerConfig.ACCESS_TOKEN; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/posts") +@SecurityRequirements +@SecurityRequirement(name = ACCESS_TOKEN) +public class CommentController { + + private final CommentService commentService; + + @PostMapping("/{post_id}/comments") + public ResponseEntity createComment( + Principal principal, + @PathVariable("post_id") Long postId, + @Valid @RequestBody CommentCreateRequest commentCreateRequest + ) { + + CommentCreateResponse commentCreateResponse = commentService.createComment( + principal.getName(), postId, commentCreateRequest); + return ResponseEntity.ok().body(commentCreateResponse); + } + + @PatchMapping("/{post_id}/comments/{comment_id}") + public ResponseEntity updateComment( + Principal principal, + @PathVariable("post_id") Long postId, + @PathVariable("comment_id") Long commentId, + @Valid @RequestBody CommentUpdateRequest commentUpdateRequest + ) { + + CommentUpdateResponse commentUpdateResponse = commentService.updateComment( + principal.getName(), postId, commentId, commentUpdateRequest + ); + return ResponseEntity.ok().body(commentUpdateResponse); + } + + @DeleteMapping("/{post_id}/comments/{comment_id}") + public ResponseEntity deleteCommentById( + Principal principal, + @PathVariable("post_id") Long postId, + @PathVariable("comment_id") Long commentId + ) { + + CommentDeleteResponse commentDeleteResponse = commentService.deleteCommentById(principal.getName(), postId, commentId); + return ResponseEntity.ok().body(commentDeleteResponse); + } + +} diff --git a/src/main/java/com/example/solidconnection/comment/domain/Comment.java b/src/main/java/com/example/solidconnection/comment/domain/Comment.java new file mode 100644 index 000000000..774c01123 --- /dev/null +++ b/src/main/java/com/example/solidconnection/comment/domain/Comment.java @@ -0,0 +1,111 @@ +package com.example.solidconnection.comment.domain; + +import com.example.solidconnection.entity.common.BaseEntity; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.siteuser.domain.SiteUser; +import jakarta.persistence.*; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@NoArgsConstructor +@EqualsAndHashCode(of = "id") +public class Comment extends BaseEntity { + + // for recursive query + @Transient + private int level; + + @Transient + private String path; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(length = 255) + private String content; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "site_user_id") + private SiteUser siteUser; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_id") + private Comment parentComment; + + @OneToMany(mappedBy = "parentComment", cascade = CascadeType.ALL) + private List commentList = new ArrayList<>(); + + public Comment(String content) { + this.content = content; + } + + public void setParentCommentAndPostAndSiteUser(Comment parentComment, Post post, SiteUser siteUser) { + + if (this.parentComment != null) { + this.parentComment.getCommentList().remove(this); + } + this.parentComment = parentComment; + parentComment.getCommentList().add(this); + + if (this.post != null) { + this.post.getCommentList().remove(this); + } + this.post = post; + post.getCommentList().add(this); + + if (this.siteUser != null) { + this.siteUser.getCommentList().remove(this); + } + this.siteUser = siteUser; + siteUser.getCommentList().add(this); + } + + public void setPostAndSiteUser(Post post, SiteUser siteUser) { + + if (this.post != null) { + this.post.getCommentList().remove(this); + } + this.post = post; + post.getCommentList().add(this); + + if (this.siteUser != null) { + this.siteUser.getCommentList().remove(this); + } + this.siteUser = siteUser; + siteUser.getCommentList().add(this); + } + + public void resetPostAndSiteUserAndParentComment() { + if (this.post != null) { + this.post.getCommentList().remove(this); + this.post = null; + } + if (this.siteUser != null) { + this.siteUser.getCommentList().remove(this); + this.siteUser = null; + } + if (this.parentComment != null) { + this.parentComment.getCommentList().remove(this); + this.parentComment = null; + } + } + + public void updateContent(String content) { + this.content = content; + } + + public void deprecateComment() { + this.content = null; + } +} diff --git a/src/main/java/com/example/solidconnection/comment/dto/CommentCreateRequest.java b/src/main/java/com/example/solidconnection/comment/dto/CommentCreateRequest.java new file mode 100644 index 000000000..c16c77323 --- /dev/null +++ b/src/main/java/com/example/solidconnection/comment/dto/CommentCreateRequest.java @@ -0,0 +1,39 @@ +package com.example.solidconnection.comment.dto; + +import com.example.solidconnection.comment.domain.Comment; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.siteuser.domain.SiteUser; +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Getter; + +@Getter +public class CommentCreateRequest { + + @NotBlank(message = "댓글 내용은 빈 값일 수 없습니다.") + @Size(min = 1, max = 255, message = "댓글 내용은 최소 1자 이상, 최대 255자 이하여야 합니다.") + String content; + + @Nullable + Long parentId; + + public CommentCreateRequest(String content, @Nullable Long parentId) { + this.content = content; + this.parentId = parentId; + } + + public Comment toEntity(SiteUser siteUser, Post post, Comment parentComment) { + + Comment comment = new Comment( + this.content + ); + + if (parentComment == null) { + comment.setPostAndSiteUser(post, siteUser); + } else { + comment.setParentCommentAndPostAndSiteUser(parentComment, post, siteUser); + } + return comment; + } +} diff --git a/src/main/java/com/example/solidconnection/comment/dto/CommentCreateResponse.java b/src/main/java/com/example/solidconnection/comment/dto/CommentCreateResponse.java new file mode 100644 index 000000000..60d7529c2 --- /dev/null +++ b/src/main/java/com/example/solidconnection/comment/dto/CommentCreateResponse.java @@ -0,0 +1,14 @@ +package com.example.solidconnection.comment.dto; + +import com.example.solidconnection.comment.domain.Comment; + +public record CommentCreateResponse( + Long id +) { + + public static CommentCreateResponse from(Comment comment) { + return new CommentCreateResponse( + comment.getId() + ); + } +} diff --git a/src/main/java/com/example/solidconnection/comment/dto/CommentDeleteResponse.java b/src/main/java/com/example/solidconnection/comment/dto/CommentDeleteResponse.java new file mode 100644 index 000000000..393e4fe8b --- /dev/null +++ b/src/main/java/com/example/solidconnection/comment/dto/CommentDeleteResponse.java @@ -0,0 +1,6 @@ +package com.example.solidconnection.comment.dto; + +public record CommentDeleteResponse( + Long id +) { +} diff --git a/src/main/java/com/example/solidconnection/comment/dto/CommentUpdateRequest.java b/src/main/java/com/example/solidconnection/comment/dto/CommentUpdateRequest.java new file mode 100644 index 000000000..23ae16118 --- /dev/null +++ b/src/main/java/com/example/solidconnection/comment/dto/CommentUpdateRequest.java @@ -0,0 +1,12 @@ +package com.example.solidconnection.comment.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record CommentUpdateRequest( + @NotBlank(message = "댓글 내용은 빈 값일 수 없습니다.") + @Size(min = 1, max = 255, message = "댓글 내용은 최소 1자 이상, 최대 255자 이하여야 합니다.") + String content +) { + +} diff --git a/src/main/java/com/example/solidconnection/comment/dto/CommentUpdateResponse.java b/src/main/java/com/example/solidconnection/comment/dto/CommentUpdateResponse.java new file mode 100644 index 000000000..b621ab111 --- /dev/null +++ b/src/main/java/com/example/solidconnection/comment/dto/CommentUpdateResponse.java @@ -0,0 +1,14 @@ +package com.example.solidconnection.comment.dto; + +import com.example.solidconnection.comment.domain.Comment; + +public record CommentUpdateResponse( + Long id +) { + + public static CommentUpdateResponse from(Comment comment) { + return new CommentUpdateResponse( + comment.getId() + ); + } +} diff --git a/src/main/java/com/example/solidconnection/comment/dto/PostFindCommentResponse.java b/src/main/java/com/example/solidconnection/comment/dto/PostFindCommentResponse.java index 75414f943..8524eb95a 100644 --- a/src/main/java/com/example/solidconnection/comment/dto/PostFindCommentResponse.java +++ b/src/main/java/com/example/solidconnection/comment/dto/PostFindCommentResponse.java @@ -1,6 +1,6 @@ package com.example.solidconnection.comment.dto; -import com.example.solidconnection.entity.Comment; +import com.example.solidconnection.comment.domain.Comment; import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse; import java.time.LocalDateTime; diff --git a/src/main/java/com/example/solidconnection/comment/repository/CommentRepository.java b/src/main/java/com/example/solidconnection/comment/repository/CommentRepository.java index 0b0d7152c..b78011903 100644 --- a/src/main/java/com/example/solidconnection/comment/repository/CommentRepository.java +++ b/src/main/java/com/example/solidconnection/comment/repository/CommentRepository.java @@ -1,12 +1,14 @@ package com.example.solidconnection.comment.repository; -import com.example.solidconnection.entity.Comment; +import com.example.solidconnection.comment.domain.Comment; +import com.example.solidconnection.custom.exception.CustomException; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.util.List; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_COMMENT_ID; public interface CommentRepository extends JpaRepository { @Query(value = """ @@ -30,4 +32,8 @@ WITH RECURSIVE CommentTree AS ( """, nativeQuery = true) List findCommentTreeByPostId(@Param("postId") Long postId); + default Comment getById(Long id) { + return findById(id) + .orElseThrow(() -> new CustomException(INVALID_COMMENT_ID)); + } } diff --git a/src/main/java/com/example/solidconnection/comment/service/CommentService.java b/src/main/java/com/example/solidconnection/comment/service/CommentService.java index 9e32e4d32..4fde0c1cf 100644 --- a/src/main/java/com/example/solidconnection/comment/service/CommentService.java +++ b/src/main/java/com/example/solidconnection/comment/service/CommentService.java @@ -1,29 +1,100 @@ package com.example.solidconnection.comment.service; +import com.example.solidconnection.comment.dto.*; import com.example.solidconnection.comment.repository.CommentRepository; -import com.example.solidconnection.comment.dto.PostFindCommentResponse; -import com.example.solidconnection.entity.Comment; +import com.example.solidconnection.comment.domain.Comment; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; +import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_UPDATE_DEPRECATED_COMMENT; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_ACCESS; + @Service @RequiredArgsConstructor public class CommentService { private final CommentRepository commentRepository; + private final SiteUserRepository siteUserRepository; + private final PostRepository postRepository; private Boolean isOwner(Comment comment, String email) { return comment.getSiteUser().getEmail().equals(email); } + private void validateOwnership(Comment comment, String email) { + if (!comment.getSiteUser().getEmail().equals(email)) { + throw new CustomException(INVALID_POST_ACCESS); + } + } + private void validateDeprecated(Comment comment) { + if (comment.getContent() == null) { + throw new CustomException(CAN_NOT_UPDATE_DEPRECATED_COMMENT); + } + } + + @Transactional(readOnly = true) public List findCommentsByPostId(String email, Long postId) { return commentRepository.findCommentTreeByPostId(postId) .stream() .map(comment -> PostFindCommentResponse.from(isOwner(comment, email), comment)) .collect(Collectors.toList()); } + + @Transactional + public CommentCreateResponse createComment(String email, Long postId, CommentCreateRequest commentCreateRequest) { + + SiteUser siteUser = siteUserRepository.getByEmail(email); + Post post = postRepository.getById(postId); + + Comment parentComment = Optional.ofNullable(commentCreateRequest.getParentId()) + .map(commentRepository::getById) + .orElse(null); + Comment createdComment = commentRepository.save(commentCreateRequest.toEntity(siteUser, post, parentComment)); + + return CommentCreateResponse.from(createdComment); + } + + @Transactional + public CommentUpdateResponse updateComment(String email, Long postId, Long commentId, CommentUpdateRequest commentUpdateRequest) { + + SiteUser siteUser = siteUserRepository.getByEmail(email); + Post post = postRepository.getById(postId); + Comment comment = commentRepository.getById(commentId); + validateDeprecated(comment); + validateOwnership(comment, email); + + comment.updateContent(commentUpdateRequest.content()); + + return CommentUpdateResponse.from(comment); + } + + @Transactional + public CommentDeleteResponse deleteCommentById(String email, Long postId, Long commentId) { + SiteUser siteUser = siteUserRepository.getByEmail(email); + Post post = postRepository.getById(postId); + Comment comment = commentRepository.getById(commentId); + validateOwnership(comment, email); + + if (comment.getCommentList().isEmpty()) { + // 하위 댓글이 없다면 삭제한다. + comment.resetPostAndSiteUserAndParentComment(); + commentRepository.deleteById(commentId); + } else { + // 하위 댓글 있으면 value만 null로 수정한다. + comment.deprecateComment(); + } + + return new CommentDeleteResponse(commentId); + } } diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index 6caf9edc2..0a9df0b61 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -58,6 +58,9 @@ public enum ErrorCode { INVALID_POST_ACCESS(HttpStatus.BAD_REQUEST.value(), "자신의 게시글만 제어할 수 있습니다."), CAN_NOT_DELETE_OR_UPDATE_QUESTION(HttpStatus.BAD_REQUEST.value(), "질문글은 수정이나 삭제할 수 없습니다."), CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES(HttpStatus.BAD_REQUEST.value(), "5개 이상의 파일을 업로드할 수 없습니다."), + INVALID_COMMENT_ID(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 댓글입니다."), + INVALID_COMMENT_ACCESS(HttpStatus.BAD_REQUEST.value(), "자신의 댓글만 제어할 수 있습니다."), + CAN_NOT_UPDATE_DEPRECATED_COMMENT(HttpStatus.BAD_REQUEST.value(),"이미 삭제된 댓글을 수정할 수 없습니다."), // general JSON_PARSING_FAILED(HttpStatus.BAD_REQUEST.value(), "JSON 파싱을 할 수 없습니다."), diff --git a/src/main/java/com/example/solidconnection/entity/Comment.java b/src/main/java/com/example/solidconnection/entity/Comment.java deleted file mode 100644 index 7b4ad87d8..000000000 --- a/src/main/java/com/example/solidconnection/entity/Comment.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.example.solidconnection.entity; - -import com.example.solidconnection.entity.common.BaseEntity; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.siteuser.domain.SiteUser; -import jakarta.persistence.*; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.util.ArrayList; -import java.util.List; - -@Entity -@Getter -@NoArgsConstructor -public class Comment extends BaseEntity { - - // for recursive query - @Transient - private int level; - - @Transient - private String path; - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(length = 255) - private String content; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "post_id") - private Post post; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "site_user_id") - private SiteUser siteUser; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "parent_id") - private Comment parentComment; - - @OneToMany(mappedBy = "parentComment", cascade = CascadeType.ALL) - private List commentList = new ArrayList<>(); -} diff --git a/src/main/java/com/example/solidconnection/post/controller/PostController.java b/src/main/java/com/example/solidconnection/post/controller/PostController.java index 022ca8b61..fd452af8a 100644 --- a/src/main/java/com/example/solidconnection/post/controller/PostController.java +++ b/src/main/java/com/example/solidconnection/post/controller/PostController.java @@ -4,6 +4,7 @@ import com.example.solidconnection.post.service.PostService; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -28,7 +29,7 @@ public class PostController { public ResponseEntity createPost( Principal principal, @PathVariable("code") String code, - @RequestPart("postCreateRequest") PostCreateRequest postCreateRequest, + @Valid @RequestPart("postCreateRequest") PostCreateRequest postCreateRequest, @RequestParam(value = "file", required = false) List imageFile) { if (imageFile == null) { @@ -44,7 +45,7 @@ public ResponseEntity updatePost( Principal principal, @PathVariable("code") String code, @PathVariable("post_id") Long postId, - @RequestPart("postUpdateRequest") PostUpdateRequest postUpdateRequest, + @Valid @RequestPart("postUpdateRequest") PostUpdateRequest postUpdateRequest, @RequestParam(value = "file", required = false) List imageFile) { if (imageFile == null) { diff --git a/src/main/java/com/example/solidconnection/post/domain/Post.java b/src/main/java/com/example/solidconnection/post/domain/Post.java index 646ac3995..12e43195f 100644 --- a/src/main/java/com/example/solidconnection/post/domain/Post.java +++ b/src/main/java/com/example/solidconnection/post/domain/Post.java @@ -1,7 +1,7 @@ package com.example.solidconnection.post.domain; import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.entity.Comment; +import com.example.solidconnection.comment.domain.Comment; import com.example.solidconnection.entity.PostImage; import com.example.solidconnection.entity.common.BaseEntity; import com.example.solidconnection.entity.mapping.PostLike; diff --git a/src/main/java/com/example/solidconnection/post/dto/PostCreateRequest.java b/src/main/java/com/example/solidconnection/post/dto/PostCreateRequest.java index 13cd6469b..03ab79686 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostCreateRequest.java +++ b/src/main/java/com/example/solidconnection/post/dto/PostCreateRequest.java @@ -4,11 +4,20 @@ import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.type.PostCategory; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; public record PostCreateRequest( + @NotNull(message = "게시글 카테고리를 설정해주세요.") String postCategory, + @NotBlank(message = "게시글 제목은 빈 값일 수 없습니다.") + @Size(min = 1, max = 255, message = "댓글 내용은 최소 1자 이상, 최대 255자 이하여야 합니다.") String title, + @NotBlank(message = "게시글 내용은 빈 값일 수 없습니다.") + @Size(min = 1, max = 1000, message = "댓글 내용은 최소 1자 이상, 최대 255자 이하여야 합니다.") String content, + @NotNull(message = "게시글 질문여부를 설정해주세요.") Boolean isQuestion ) { diff --git a/src/main/java/com/example/solidconnection/post/dto/PostUpdateRequest.java b/src/main/java/com/example/solidconnection/post/dto/PostUpdateRequest.java index 9394932d7..b82b73685 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostUpdateRequest.java +++ b/src/main/java/com/example/solidconnection/post/dto/PostUpdateRequest.java @@ -1,8 +1,17 @@ package com.example.solidconnection.post.dto; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + public record PostUpdateRequest( + @NotNull(message = "게시글 카테고리를 설정해주세요.") String postCategory, + @NotBlank(message = "게시글 제목은 빈 값일 수 없습니다.") + @Size(min = 1, max = 255, message = "댓글 내용은 최소 1자 이상, 최대 255자 이하여야 합니다.") String title, + @NotBlank(message = "게시글 내용은 빈 값일 수 없습니다.") + @Size(min = 1, max = 1000, message = "댓글 내용은 최소 1자 이상, 최대 255자 이하여야 합니다.") String content ) { } diff --git a/src/main/java/com/example/solidconnection/post/service/PostService.java b/src/main/java/com/example/solidconnection/post/service/PostService.java index 52bd22310..d4ed3c081 100644 --- a/src/main/java/com/example/solidconnection/post/service/PostService.java +++ b/src/main/java/com/example/solidconnection/post/service/PostService.java @@ -19,8 +19,10 @@ import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.BoardCode; import com.example.solidconnection.type.ImgType; +import com.example.solidconnection.type.PostCategory; import com.example.solidconnection.util.RedisUtils; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.EnumUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -69,6 +71,12 @@ private void validateQuestion(Post post) { } } + private void validatePostCategory(String category){ + if(!EnumUtils.isValidEnum(PostCategory.class, category)){ + throw new CustomException(INVALID_POST_CATEGORY); + } + } + private Boolean getIsOwner(Post post, String email) { return post.getSiteUser().getEmail().equals(email); } @@ -79,6 +87,7 @@ public PostCreateResponse createPost(String email, String code, PostCreateReques // 유효성 검증 String boardCode = validateCode(code); + validatePostCategory(postCreateRequest.postCategory()); validateFileSize(imageFile); // 객체 생성 @@ -146,7 +155,7 @@ public PostFindResponse findPostById(String email, String code, Long postId) { // caching && 어뷰징 방지 if (redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(email,postId))) { - redisService.increaseViewCountSync(redisUtils.getPostViewCountRedisKey(postId)); + redisService.increaseViewCount(redisUtils.getPostViewCountRedisKey(postId)); } return PostFindResponse.from( diff --git a/src/main/java/com/example/solidconnection/service/RedisService.java b/src/main/java/com/example/solidconnection/service/RedisService.java index 4776f1692..9816a264e 100644 --- a/src/main/java/com/example/solidconnection/service/RedisService.java +++ b/src/main/java/com/example/solidconnection/service/RedisService.java @@ -24,7 +24,7 @@ public RedisService(RedisTemplate redisTemplate, } // incr & set ttl -> lua - public void increaseViewCountSync(String key) { + public void increaseViewCount(String key) { redisTemplate.execute(incrViewCountLuaScript, Collections.singletonList(key), VIEW_COUNT_TTL.getValue()); } diff --git a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java index 2cd7dc4ff..300c69492 100644 --- a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java +++ b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java @@ -1,6 +1,6 @@ package com.example.solidconnection.siteuser.domain; -import com.example.solidconnection.entity.Comment; +import com.example.solidconnection.comment.domain.Comment; import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.entity.mapping.PostLike; import com.example.solidconnection.type.Gender; @@ -59,7 +59,7 @@ public class SiteUser { @OneToMany(mappedBy = "siteUser", cascade = CascadeType.ALL, orphanRemoval = true) private List postList = new ArrayList<>(); - @OneToMany(mappedBy = "siteUser", cascade = CascadeType.ALL, orphanRemoval = true) + @OneToMany(mappedBy = "siteUser", cascade = CascadeType.ALL) private List commentList = new ArrayList<>(); @OneToMany(mappedBy = "siteUser", cascade = CascadeType.ALL, orphanRemoval = true) diff --git a/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java b/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java index 9eb936301..c2213993d 100644 --- a/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java +++ b/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java @@ -98,13 +98,15 @@ private Post createPost(Board board, SiteUser siteUser) { @Test public void 게시글을_조회할_때_조회수_동시성_문제를_해결한다() throws InterruptedException { + redisService.deleteKey(redisUtils.getValidatePostViewCountRedisKey(siteUser.getEmail(), post.getId())); + ExecutorService executorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE); CountDownLatch doneSignal = new CountDownLatch(THREAD_NUMS); for (int i = 0; i < THREAD_NUMS; i++) { executorService.submit(() -> { try { - redisService.increaseViewCountSync(redisUtils.getPostViewCountRedisKey(post.getId())); + redisService.increaseViewCount(redisUtils.getPostViewCountRedisKey(post.getId())); } finally { doneSignal.countDown(); } @@ -118,7 +120,7 @@ private Post createPost(Board board, SiteUser siteUser) { System.err.println("ExecutorService did not terminate in the expected time."); } - Thread.sleep(SCHEDULING_DELAY_MS); + Thread.sleep(SCHEDULING_DELAY_MS+1000); assertEquals(THREAD_NUMS, postRepository.getById(post.getId()).getViewCount()); } @@ -136,7 +138,7 @@ private Post createPost(Board board, SiteUser siteUser) { try { boolean isFirstTime = redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(siteUser.getEmail(), post.getId())); if (isFirstTime) { - redisService.increaseViewCountSync(redisUtils.getPostViewCountRedisKey(post.getId())); + redisService.increaseViewCount(redisUtils.getPostViewCountRedisKey(post.getId())); } } finally { doneSignal.countDown(); @@ -149,7 +151,7 @@ private Post createPost(Board board, SiteUser siteUser) { try { boolean isFirstTime = redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(siteUser.getEmail(), post.getId())); if (isFirstTime) { - redisService.increaseViewCountSync(redisUtils.getPostViewCountRedisKey(post.getId())); + redisService.increaseViewCount(redisUtils.getPostViewCountRedisKey(post.getId())); } } finally { doneSignal.countDown(); @@ -164,7 +166,7 @@ private Post createPost(Board board, SiteUser siteUser) { System.err.println("ExecutorService did not terminate in the expected time."); } - Thread.sleep(SCHEDULING_DELAY_MS); + Thread.sleep(SCHEDULING_DELAY_MS+1000); assertEquals(2L, postRepository.getById(post.getId()).getViewCount()); } diff --git a/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java index c540954b1..9ea7ee0d9 100644 --- a/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java @@ -27,7 +27,7 @@ @DataJpaTest @ActiveProfiles("test") @DisplayName("게시판 레포지토리 테스트") -public class BoardRepositoryTest { +class BoardRepositoryTest { @Autowired private PostRepository postRepository; @Autowired diff --git a/src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java new file mode 100644 index 000000000..7029dead9 --- /dev/null +++ b/src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java @@ -0,0 +1,156 @@ +package com.example.solidconnection.unit.repository; + +import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.board.repository.BoardRepository; +import com.example.solidconnection.comment.domain.Comment; +import com.example.solidconnection.comment.repository.CommentRepository; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PostCategory; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import jakarta.persistence.EntityManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_COMMENT_ID; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +@SpringBootTest +@ActiveProfiles("local") +@DisplayName("댓글 레포지토리 테스트") +class CommentRepositoryTest { + @Autowired + private PostRepository postRepository; + @Autowired + private BoardRepository boardRepository; + @Autowired + private SiteUserRepository siteUserRepository; + @Autowired + private EntityManager entityManager; + @Autowired + private CommentRepository commentRepository; + + private Board board; + private SiteUser siteUser; + private Post post; + private Comment parentComment; + private Comment childComment; + + @BeforeEach + public void setUp() { + board = createBoard(); + boardRepository.save(board); + + siteUser = createSiteUser(); + siteUserRepository.save(siteUser); + + post = createPost(board, siteUser); + post = postRepository.save(post); + + parentComment = createParentComment(); + childComment = createChildComment(); + commentRepository.save(parentComment); + commentRepository.save(childComment); + + entityManager.flush(); + entityManager.clear(); + } + + private Board createBoard() { + return new Board( + "FREE", "자유게시판"); + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } + + private Post createPost(Board board, SiteUser siteUser) { + Post post = new Post( + "title", + "content", + false, + 0L, + 0L, + PostCategory.valueOf("자유") + ); + post.setBoardAndSiteUser(board, siteUser); + return post; + } + + private Comment createParentComment() { + Comment comment = new Comment( + "parent" + ); + comment.setPostAndSiteUser(post, siteUser); + return comment; + } + + private Comment createChildComment() { + Comment comment = new Comment( + "child" + ); + comment.setParentCommentAndPostAndSiteUser(parentComment, post, siteUser); + return comment; + } + + @Test + @Transactional + public void 재귀쿼리로_댓글트리를_조회한다() { + // when + List commentTreeByPostId = commentRepository.findCommentTreeByPostId(post.getId()); + + // then + List expectedResponse = List.of(parentComment, childComment); + assertEquals(commentTreeByPostId, expectedResponse); + } + + @Test + @Transactional + public void 댓글을_조회한다() { + // when + Comment foundComment = commentRepository.getById(parentComment.getId()); + + // then + assertEquals(parentComment, foundComment); + } + + @Test + @Transactional + public void 댓글을_조회할_때_유효한_댓글이_아니라면_예외_응답을_반환한다() { + // given + Long invalidId = -1L; + + // when, then + CustomException exception = assertThrows(CustomException.class, () -> { + commentRepository.getById(invalidId); + }); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_COMMENT_ID.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_COMMENT_ID.getCode()); + } +} diff --git a/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java index b81ee952c..5fa873d06 100644 --- a/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java @@ -31,7 +31,7 @@ @DataJpaTest @ActiveProfiles("test") @DisplayName("게시글 레포지토리 테스트") -public class PostRepositoryTest { +class PostRepositoryTest { @Autowired private PostRepository postRepository; @Autowired diff --git a/src/test/java/com/example/solidconnection/unit/service/BoardServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/BoardServiceTest.java index 710546c9b..18c37b807 100644 --- a/src/test/java/com/example/solidconnection/unit/service/BoardServiceTest.java +++ b/src/test/java/com/example/solidconnection/unit/service/BoardServiceTest.java @@ -27,7 +27,7 @@ @ExtendWith(MockitoExtension.class) @DisplayName("게시판 서비스 테스트") -public class BoardServiceTest { +class BoardServiceTest { @InjectMocks BoardService boardService; @Mock diff --git a/src/test/java/com/example/solidconnection/unit/service/CommentServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/CommentServiceTest.java new file mode 100644 index 000000000..8a90b275b --- /dev/null +++ b/src/test/java/com/example/solidconnection/unit/service/CommentServiceTest.java @@ -0,0 +1,423 @@ +package com.example.solidconnection.unit.service; + +import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.comment.domain.Comment; +import com.example.solidconnection.comment.dto.*; +import com.example.solidconnection.comment.repository.CommentRepository; +import com.example.solidconnection.comment.service.CommentService; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.stream.Collectors; + +import static com.example.solidconnection.custom.exception.ErrorCode.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("댓글 서비스 테스트") +class CommentServiceTest { + @InjectMocks + CommentService commentService; + @Mock + PostRepository postRepository; + @Mock + SiteUserRepository siteUserRepository; + @Mock + CommentRepository commentRepository; + + private SiteUser siteUser; + private Board board; + private Post post; + private Comment parentComment_1; + private Comment parentComment_2; + private Comment p1s_childComment; + + + @BeforeEach + void setUp() { + siteUser = createSiteUser(); + board = createBoard(); + post = createPost(board, siteUser); + parentComment_1 = createParentComment(); + parentComment_2 = createParentComment(); + p1s_childComment = createChildComment(); + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } + + private Board createBoard() { + return new Board( + "FREE", "자유게시판"); + } + + private Post createPost(Board board, SiteUser siteUser) { + Post post = new Post( + "title", + "content", + false, + 0L, + 0L, + PostCategory.valueOf("자유") + ); + post.setBoardAndSiteUser(board, siteUser); + return post; + } + + private Comment createParentComment() { + Comment comment = new Comment( + "parent" + ); + comment.setPostAndSiteUser(post, siteUser); + return comment; + } + + private Comment createChildComment() { + Comment comment = new Comment( + "child" + ); + comment.setParentCommentAndPostAndSiteUser(parentComment_1, post, siteUser); + return comment; + } + + /** + * 댓글 조회 + */ + + @Test + void 특정_게시글의_댓글들을_조회한다() { + // Given + List commentList = List.of(parentComment_1, p1s_childComment, parentComment_2); + when(commentRepository.findCommentTreeByPostId(post.getId())).thenReturn(commentList); + + // When + List postFindCommentResponses = commentService.findCommentsByPostId( + siteUser.getEmail(), post.getId()); + + // Then + List expectedResponse = commentList.stream() + .map(comment -> PostFindCommentResponse.from(isOwner(comment, siteUser.getEmail()), comment)) + .collect(Collectors.toList()); + assertEquals(postFindCommentResponses, expectedResponse); + } + + private Boolean isOwner(Comment comment, String email) { + return comment.getSiteUser().getEmail().equals(email); + } + + /** + * 댓글 등록 + */ + @Test + void 부모_댓글을_등록한다() { + // Given + CommentCreateRequest commentCreateRequest = new CommentCreateRequest( + "parent", null + ); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.save(any(Comment.class))).thenReturn(parentComment_1); + + // When + CommentCreateResponse commentCreateResponse = commentService.createComment( + siteUser.getEmail(), post.getId(), commentCreateRequest); + + // Then + assertEquals(commentCreateResponse, CommentCreateResponse.from(parentComment_1)); + verify(commentRepository, times(0)) + .getById(any(Long.class)); + verify(commentRepository, times(1)) + .save(commentCreateRequest.toEntity(siteUser, post, parentComment_1)); + } + + @Test + void 자식_댓글을_등록한다() { + // Given + Long parentCommentId = 1L; + CommentCreateRequest commentCreateRequest = new CommentCreateRequest( + "child", parentCommentId + ); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(parentCommentId)).thenReturn(parentComment_1); + when(commentRepository.save(any(Comment.class))).thenReturn(p1s_childComment); + + // When + CommentCreateResponse commentCreateResponse = commentService.createComment( + siteUser.getEmail(), post.getId(), commentCreateRequest); + + // Then + assertEquals(commentCreateResponse, CommentCreateResponse.from(p1s_childComment)); + verify(commentRepository, times(1)) + .getById(parentCommentId); + verify(commentRepository, times(1)) + .save(commentCreateRequest.toEntity(siteUser, post, parentComment_1)); + } + + + @Test + void 댓글을_등록할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { + // Given + Long invalidPostId = -1L; + CommentCreateRequest commentCreateRequest = new CommentCreateRequest( + "child", null + ); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + commentService.createComment(siteUser.getEmail(), invalidPostId, commentCreateRequest) + ); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_POST_ID.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_POST_ID.getCode()); + verify(commentRepository, times(0)) + .save(any(Comment.class)); + } + + @Test + void 댓글을_등록할_때_유효한_부모_댓글이_아니라면_예외_응답을_반환한다() { + // Given + Long invalidParentCommentId = -1L; + CommentCreateRequest commentCreateRequest = new CommentCreateRequest( + "child", invalidParentCommentId + ); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(invalidParentCommentId)).thenThrow(new CustomException(INVALID_COMMENT_ID)); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + commentService.createComment(siteUser.getEmail(), post.getId(), commentCreateRequest) + ); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_COMMENT_ID.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_COMMENT_ID.getCode()); + verify(commentRepository, times(0)) + .save(any(Comment.class)); + } + + /** + * 댓글 수정 + */ + @Test + void 댓글을_수정한다() { + // Given + CommentUpdateRequest commentUpdateRequest = new CommentUpdateRequest( + "update" + ); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(any())).thenReturn(parentComment_1); + + // When + CommentUpdateResponse commentUpdateResponse = commentService.updateComment( + siteUser.getEmail(), post.getId(), parentComment_1.getId(), commentUpdateRequest); + + // Then + assertEquals(commentUpdateResponse.id(), parentComment_1.getId()); + } + + @Test + void 댓글을_수정할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { + // Given + Long invalidPostId = -1L; + CommentUpdateRequest commentUpdateRequest = new CommentUpdateRequest( + "update" + ); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + commentService.updateComment(siteUser.getEmail(), invalidPostId, parentComment_1.getId(), commentUpdateRequest) + ); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_POST_ID.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_POST_ID.getCode()); + } + + @Test + void 댓글을_수정할_때_유효한_댓글이_아니라면_예외_응답을_반환한다() { + // Given + Long invalidCommentId = -1L; + CommentUpdateRequest commentUpdateRequest = new CommentUpdateRequest( + "update" + ); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(invalidCommentId)).thenThrow(new CustomException(INVALID_COMMENT_ID)); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + commentService.updateComment(siteUser.getEmail(), post.getId(), invalidCommentId, commentUpdateRequest) + ); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_COMMENT_ID.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_COMMENT_ID.getCode()); + } + + @Test + void 댓글을_수정할_때_이미_삭제된_댓글이라면_예외_응답을_반환한다() { + // Given + parentComment_1.deprecateComment(); + CommentUpdateRequest commentUpdateRequest = new CommentUpdateRequest( + "update" + ); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(any())).thenReturn(parentComment_1); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + commentService.updateComment(siteUser.getEmail(), post.getId(), parentComment_1.getId(), commentUpdateRequest) + ); + assertThat(exception.getMessage()) + .isEqualTo(CAN_NOT_UPDATE_DEPRECATED_COMMENT.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(CAN_NOT_UPDATE_DEPRECATED_COMMENT.getCode()); + } + + @Test + void 댓글을_수정할_때_자신의_댓글이_아니라면_예외_응답을_반환한다() { + // Given + String invalidEmail = "invalidEmail@test.com"; + CommentUpdateRequest commentUpdateRequest = new CommentUpdateRequest( + "update" + ); + when(siteUserRepository.getByEmail(invalidEmail)).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(any())).thenReturn(parentComment_1); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + commentService.updateComment(invalidEmail, post.getId(), parentComment_1.getId(), commentUpdateRequest) + ); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_POST_ACCESS.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_POST_ACCESS.getCode()); + } + + /** + * 댓글 삭제 + */ + + @Test + void 댓글을_삭제한다_자식댓글_있음() { + // Given + Long parentCommentId = 1L; + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(any())).thenReturn(parentComment_1); + + // When + CommentDeleteResponse commentDeleteResponse = commentService.deleteCommentById( + siteUser.getEmail(), post.getId(), parentCommentId); + + // Then + assertEquals(parentComment_1.getContent(), null); + assertEquals(commentDeleteResponse.id(), parentCommentId); + verify(commentRepository, times(0)).deleteById(parentCommentId); + } + + @Test + void 댓글을_삭제한다_자식댓글_없음() { + // Given + Long childCommentId = 1L; + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(any())).thenReturn(p1s_childComment); + + // When + CommentDeleteResponse commentDeleteResponse = commentService.deleteCommentById( + siteUser.getEmail(), post.getId(), childCommentId); + + // Then + assertEquals(commentDeleteResponse.id(), childCommentId); + verify(commentRepository, times(1)).deleteById(childCommentId); + } + + @Test + void 댓글을_삭제할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { + // Given + Long invalidPostId = -1L; + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + commentService.deleteCommentById(siteUser.getEmail(), invalidPostId, parentComment_1.getId()) + ); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_POST_ID.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_POST_ID.getCode()); + } + + @Test + void 댓글을_삭제할_때_유효한_댓글이_아니라면_예외_응답을_반환한다() { + // Given + Long invalidCommentId = -1L; + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(invalidCommentId)).thenThrow(new CustomException(INVALID_COMMENT_ID)); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + commentService.deleteCommentById(siteUser.getEmail(), post.getId(), invalidCommentId) + ); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_COMMENT_ID.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_COMMENT_ID.getCode()); + } + + @Test + void 댓글을_삭제할_때_자신의_댓글이_아니라면_예외_응답을_반환한다() { + // Given + String invalidEmail = "invalidEmail@test.com"; + when(siteUserRepository.getByEmail(invalidEmail)).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(any())).thenReturn(parentComment_1); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + commentService.deleteCommentById(invalidEmail, post.getId(), parentComment_1.getId()) + ); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_POST_ACCESS.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_POST_ACCESS.getCode()); + } +} diff --git a/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java index c04c1485a..7f19707b7 100644 --- a/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java +++ b/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java @@ -43,7 +43,7 @@ @ExtendWith(MockitoExtension.class) @DisplayName("게시글 서비스 테스트") -public class PostServiceTest { +class PostServiceTest { @InjectMocks PostService postService; @Mock @@ -241,6 +241,22 @@ private List createMockImageFilesWithMoreThanFiveFiles() { .isEqualTo(INVALID_BOARD_CODE.getCode()); } + @Test + void 게시글을_등록할_때_유효한_카테고리가_아니라면_예외_응답을_반환한다() { + // Given + String invalidPostCategory = "invalidPostCategory"; + PostCreateRequest postCreateRequest = new PostCreateRequest( + invalidPostCategory, "title", "content", false); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> postService + .createPost(siteUser.getEmail(), board.getCode(), postCreateRequest, Collections.emptyList())); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_POST_CATEGORY.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_POST_CATEGORY.getCode()); + } + @Test void 게시글을_등록할_때_파일_수가_5개를_넘는다면_예외_응답을_반환한다() { // Given