diff --git a/back/src/main/java/com/back/domain/post/comment/controller/PostCommentController.java b/back/src/main/java/com/back/domain/post/comment/controller/PostCommentController.java index 2655cca6..ca785e38 100644 --- a/back/src/main/java/com/back/domain/post/comment/controller/PostCommentController.java +++ b/back/src/main/java/com/back/domain/post/comment/controller/PostCommentController.java @@ -9,6 +9,7 @@ import com.back.global.rq.Rq; import com.back.global.rsData.RsData; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import jakarta.validation.constraints.Positive; import lombok.RequiredArgsConstructor; @@ -20,6 +21,7 @@ @RestController @RequestMapping("/post/comment") + public class PostCommentController { @Autowired private Rq rq; @@ -66,5 +68,12 @@ public RsData updatePostComment(@PathVariable Long post_id return new RsData<>("200", "댓글 수정 성공", null); } + @Operation(summary = "댓글 채택") + @PostMapping("isAdopted/{commentId}") + public RsData adoptComment(@PathVariable Long commentId) { + Member member = rq.getActor(); + postCommentService.adoptComment(commentId, member); + return new RsData<>("200", "댓글 채택 성공", null); + } } diff --git a/back/src/main/java/com/back/domain/post/comment/entity/PostComment.java b/back/src/main/java/com/back/domain/post/comment/entity/PostComment.java index 2e800ca2..9520f8c3 100644 --- a/back/src/main/java/com/back/domain/post/comment/entity/PostComment.java +++ b/back/src/main/java/com/back/domain/post/comment/entity/PostComment.java @@ -27,6 +27,8 @@ public class PostComment extends BaseEntity { private String role; + private Boolean isAdopted; + public Boolean isAuthor( Member member) { return Objects.equals(this.member.getId(), member.getId()); } @@ -41,6 +43,7 @@ public PostComment(Post post, String content, Member member, String role) { this.content = content; this.member = member; this.role = role; + this.isAdopted = false; } @@ -55,5 +58,14 @@ public void updatePost(Post post) { this.post = post; } + public void adoptComment() { + this.isAdopted = true; + } +/* +* Post 단위 테스트 작성 +* isAdopted 테스트 +* PracticePost 작성 권한 테스트 +* API 명세서 수정 +* */ } diff --git a/back/src/main/java/com/back/domain/post/comment/repository/PostCommentRepository.java b/back/src/main/java/com/back/domain/post/comment/repository/PostCommentRepository.java index d4c5ecc0..24b05542 100644 --- a/back/src/main/java/com/back/domain/post/comment/repository/PostCommentRepository.java +++ b/back/src/main/java/com/back/domain/post/comment/repository/PostCommentRepository.java @@ -1,6 +1,7 @@ package com.back.domain.post.comment.repository; import com.back.domain.post.comment.entity.PostComment; +import com.back.domain.post.post.entity.Post; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -13,4 +14,7 @@ public interface PostCommentRepository extends JpaRepository @Query("SELECT c FROM PostComment c JOIN FETCH c.member WHERE c.post.id = :postId") List findCommentsWithMemberByPostId(@Param("postId") Long postId); + + + boolean existsByPostAndIsAdoptedTrue(Post post); } diff --git a/back/src/main/java/com/back/domain/post/comment/service/PostCommentService.java b/back/src/main/java/com/back/domain/post/comment/service/PostCommentService.java index 74df7626..9e803f05 100644 --- a/back/src/main/java/com/back/domain/post/comment/service/PostCommentService.java +++ b/back/src/main/java/com/back/domain/post/comment/service/PostCommentService.java @@ -63,6 +63,10 @@ public void removePostComment(Long postId, CommentDeleteRequest commentDeleteReq throw new ServiceException("400", "삭제 권한이 없습니다."); } +// if(postComment.getIsAdopted()) { +// throw new ServiceException("400", "채택된 댓글은 삭제할 수 없습니다."); +// } + postCommentRepository.delete(postComment); } @@ -79,15 +83,25 @@ public void updatePostComment(Long postId, CommentModifyRequest commentModifyReq throw new ServiceException("400", "수정 권한이 없습니다."); } + if ( commentModifyRequest.getContent() == null || commentModifyRequest.getContent().isEmpty()) { + throw new ServiceException("400", "댓글은 비어 있을 수 없습니다."); + } + postComment.updateContent(commentModifyRequest.getContent()); } private void validatePostExists(Long postId) { + if(postId == null || postId <= 0) { + throw new ServiceException("400", "유효하지 않은 게시글 Id입니다."); + } + if (!postRepository.existsById(postId)) { throw new ServiceException("400", "해당 Id의 게시글이 없습니다."); } + + } private PostComment getPostCommentById(Long commentId) { @@ -95,5 +109,30 @@ private PostComment getPostCommentById(Long commentId) { } + public void adoptComment(Long commentId, Member member) { + PostComment postComment = postCommentRepository.findById(commentId) + .orElseThrow(() -> new ServiceException("400", "해당 Id의 댓글이 없습니다.")); + Post post = postComment.getPost(); + + if (!post.isAuthor(member)) { + throw new ServiceException("400", "채택 권한이 없습니다."); + } + + if (post.getPostType() != Post.PostType.QUESTIONPOST) { + throw new ServiceException("400", "질문 게시글에만 댓글 채택이 가능합니다."); + } + + if (postComment.getIsAdopted()) { + throw new ServiceException("400", "이미 채택된 댓글입니다."); + } + + // 이미 채택된 댓글이 있는지 확인 + boolean alreadyAdopted = postCommentRepository.existsByPostAndIsAdoptedTrue(post); + if (alreadyAdopted) { + throw new ServiceException("400", "이미 채택된 댓글이 있습니다."); + } + + postComment.adoptComment(); + } } diff --git a/back/src/main/java/com/back/domain/post/post/controller/InformationPostController.java b/back/src/main/java/com/back/domain/post/post/controller/PostController.java similarity index 95% rename from back/src/main/java/com/back/domain/post/post/controller/InformationPostController.java rename to back/src/main/java/com/back/domain/post/post/controller/PostController.java index eb256013..316a341e 100644 --- a/back/src/main/java/com/back/domain/post/post/controller/InformationPostController.java +++ b/back/src/main/java/com/back/domain/post/post/controller/PostController.java @@ -17,9 +17,9 @@ import java.util.List; @RestController -@RequestMapping("post/infor") +@RequestMapping("/post") @RequiredArgsConstructor -public class InformationPostController { +public class PostController { private final PostLikeService postLikeService; private final PostService postService; private final Rq rq; @@ -27,13 +27,14 @@ public class InformationPostController { @Operation(summary = "게시글 조회 - 페이징 처리") - @GetMapping + @GetMapping("/page/{postType}") public RsData getPostWithPage( + @PathVariable Post.PostType postType, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size, @RequestParam(required = false) String keyword ) { - Page postPage = postService.getPosts(keyword, page,size); + Page postPage = postService.getPosts(keyword, page,size, postType); PostPagingResponse resDto = PostPagingResponse.from(postPage); return new RsData<>("200", "게시글이 조회 되었습니다.", resDto); @@ -108,7 +109,7 @@ public RsData getLike(@PathVariable Long post_id) { } @Operation(summary = "게시글 싫어요 (Show)") - @GetMapping("/{post_id}/Disliked") + @GetMapping("/{post_id}/disliked") public RsData getDisLike(@PathVariable Long post_id) { int likeCount = postLikeService.getDisLikeCount(post_id); PostLikedResponse postLikedResponse = new PostLikedResponse(likeCount); @@ -124,6 +125,8 @@ public RsData disLikePost(@PathVariable Long post_id) { return new RsData<>("200", "게시글 싫어요 성공", null); } + + @Operation(summary = "게시글 상세페이지") @GetMapping("/Detail/{post_id}") public RsData getPostDetail(@PathVariable Long post_id) { diff --git a/back/src/main/java/com/back/domain/post/post/entity/Post.java b/back/src/main/java/com/back/domain/post/post/entity/Post.java index 62bdc2ac..b44c49fc 100644 --- a/back/src/main/java/com/back/domain/post/post/entity/Post.java +++ b/back/src/main/java/com/back/domain/post/post/entity/Post.java @@ -82,20 +82,20 @@ public static void validPostType(String postTypeStr) { try { Post.PostType.valueOf(postTypeStr); } catch (IllegalArgumentException e) { - throw new ServiceException("400-2", "유효하지 않은 PostType입니다."); + throw new ServiceException("400", "유효하지 않은 PostType입니다."); } } public void updateTitle(String title) { if(title == null || title.isBlank()) { - throw new ServiceException("400-3", "제목은 null이거나 공백일 수 없습니다."); + throw new ServiceException("400", "제목은 null이거나 공백일 수 없습니다."); } this.title = title; } public void updateContent(String content) { if(content == null || content.isBlank()) { - throw new ServiceException("400-4", "내용은 null이거나 공백일 수 없습니다."); + throw new ServiceException("400", "내용은 null이거나 공백일 수 없습니다."); } this.content = content; } @@ -103,4 +103,8 @@ public void updateContent(String content) { public void increaseViewCount() { this.viewCount ++; } + + public void updateResolveStatus(Boolean isResolve) { + this.isResolve = isResolve; + } } diff --git a/back/src/main/java/com/back/domain/post/post/repository/PostRepositoryCustom.java b/back/src/main/java/com/back/domain/post/post/repository/PostRepositoryCustom.java index 52ed920d..201766cd 100644 --- a/back/src/main/java/com/back/domain/post/post/repository/PostRepositoryCustom.java +++ b/back/src/main/java/com/back/domain/post/post/repository/PostRepositoryCustom.java @@ -5,5 +5,5 @@ import org.springframework.data.domain.Pageable; public interface PostRepositoryCustom { - Page searchPosts(String keyword, Pageable pageable); + Page searchPosts(String keyword, Pageable pageable, Post.PostType postType); } diff --git a/back/src/main/java/com/back/domain/post/post/repository/PostRepositoryImpl.java b/back/src/main/java/com/back/domain/post/post/repository/PostRepositoryImpl.java index 77101a69..2aa6bef8 100644 --- a/back/src/main/java/com/back/domain/post/post/repository/PostRepositoryImpl.java +++ b/back/src/main/java/com/back/domain/post/post/repository/PostRepositoryImpl.java @@ -17,12 +17,15 @@ public class PostRepositoryImpl implements PostRepositoryCustom{ private final JPAQueryFactory queryFactory; @Override - public Page searchPosts(String keyword, Pageable pageable) { + public Page searchPosts(String keyword, Pageable pageable, Post.PostType postType) { QPost post = QPost.post; - QMember member = QMember.member; BooleanBuilder builder = new BooleanBuilder(); + if(postType != null) { + builder.and(post.postType.eq(postType)); + } + if(keyword != null && !keyword.isBlank()) { builder.and( post.title.containsIgnoreCase(keyword) @@ -34,7 +37,7 @@ public Page searchPosts(String keyword, Pageable pageable) { .selectFrom(post) .where(builder) .orderBy(post.createDate.desc()) - .offset(pageable.getOffset())// 이거뭐임 + .offset(pageable.getOffset()) // 시작점 .limit(pageable.getPageSize()) .fetch(); diff --git a/back/src/main/java/com/back/domain/post/post/service/PostService.java b/back/src/main/java/com/back/domain/post/post/service/PostService.java index 3ae4a355..417db201 100644 --- a/back/src/main/java/com/back/domain/post/post/service/PostService.java +++ b/back/src/main/java/com/back/domain/post/post/service/PostService.java @@ -7,6 +7,7 @@ import com.back.domain.post.post.entity.Post; import com.back.domain.post.post.repository.PostRepository; import com.back.global.exception.ServiceException; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -19,6 +20,7 @@ @Service @RequiredArgsConstructor +@Tag(name = "PostController", description = "커뮤니티(게시글) API") public class PostService { private final PostRepository postRepository;; @@ -35,6 +37,17 @@ public Post createPost(PostCreateRequest postCreateRequest, Member member) { Post.validPostType(postTypeStr); Post.PostType postType = Post.PostType.valueOf(postTypeStr); + // PostType이 PracticePost인 경우 멘토인지 확인 + if (postType == Post.PostType.PRACTICEPOST && member.getRole() != Member.Role.MENTOR) { + throw new ServiceException("400", "실무 경험 공유 게시글은 멘토만 작성할 수 있습니다."); + } + +// if( postType == Post.PostType.PRACTICEPOST ) { +// if(member.getCareer() == null || member.getCareer().isEmpty()) { +// throw new ServiceException("400", "멘토는 경력을 입력해야 실무 경험 공유 게시글을 작성할 수 있습니다."); +// } +// } + Post post = Post.builder() .title(postCreateRequest.getTitle()) .content(postCreateRequest.getContent()) @@ -42,10 +55,11 @@ public Post createPost(PostCreateRequest postCreateRequest, Member member) { .postType(postType) .build(); -// post.setTitle(postCreateRequest.getTitle()); -// post.setContent(postCreateRequest.getContent()); -// post.setMember(member); -// post.setPostType(postType); + + // PostType이 QUESTIONPOST인 경우 isResolve를 false로 초기화 + if(postType == Post.PostType.QUESTIONPOST) { + post.updateResolveStatus(false); + } postRepository.save(post); @@ -65,6 +79,14 @@ public void updatePost(long postId, Member member, @Valid PostCreateRequest post Post post = findById(postId); if (!post.isAuthor(member)) throw new ServiceException("400", "수정 권한이 없습니다."); + if ( postCreateRequest.getTitle() == null || postCreateRequest.getTitle().isBlank()) { + throw new ServiceException("400", "제목을 입력해주세요."); + } + + if ( postCreateRequest.getContent() == null || postCreateRequest.getContent().isBlank()) { + throw new ServiceException("400", "내용을 입력해주세요."); + } + post.updateTitle(postCreateRequest.getTitle()); post.updateContent(postCreateRequest.getContent()); @@ -79,11 +101,10 @@ public Post getPostDetailWithViewIncrement(Long postId) { return post; } - - public Page getPosts(String keyword, int page, int size) { + public Page getPosts(String keyword, int page, int size ,Post.PostType postType) { Pageable pageable = PageRequest.of(page, size); - return postRepository.searchPosts(keyword, pageable).map(PostDto::from); + return postRepository.searchPosts(keyword, pageable, postType).map(PostDto::from); } public Post findById(Long postId) { @@ -91,13 +112,12 @@ public Post findById(Long postId) { return post; } - - public List getAllPostResponse() { return postRepository.findAllWithMember().stream() .map(PostAllResponse::new) .toList(); } + //채택된 comment 받아오기 } diff --git a/back/src/main/java/com/back/domain/post/rq/PostDetailFacade.java b/back/src/main/java/com/back/domain/post/rq/PostDetailFacade.java index b1291426..11b67110 100644 --- a/back/src/main/java/com/back/domain/post/rq/PostDetailFacade.java +++ b/back/src/main/java/com/back/domain/post/rq/PostDetailFacade.java @@ -33,9 +33,5 @@ public PostDetailResponse getDetailWithViewIncrement(Long postId) { return PostDetailResponse.from(post, comments, likeCount, dislikeCount, userStatus); } -// public Post findById(Long postId) { -// return postService.findById(postId); -// } - } diff --git a/back/src/test/java/com/back/domain/post/comment/entity/PostCommentTest.java b/back/src/test/java/com/back/domain/post/comment/entity/PostCommentTest.java new file mode 100644 index 00000000..fc781b86 --- /dev/null +++ b/back/src/test/java/com/back/domain/post/comment/entity/PostCommentTest.java @@ -0,0 +1,318 @@ +package com.back.domain.post.comment.entity; + +import com.back.domain.member.member.entity.Member; +import com.back.domain.post.post.entity.Post; +import com.back.fixture.MemberFixture; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +class PostCommentTest { + + @Nested + @DisplayName("PostComment 생성 테스트") + class CreateCommentTest { + + @Test + @DisplayName("정상적인 PostComment 생성") + void createComment_success() { + // given + Member member = MemberFixture.createDefault(); + Post post = createDefaultPost(member); + String content = "테스트 댓글"; + String role = member.getRole().name(); + + // when + PostComment comment = PostComment.builder() + .post(post) + .content(content) + .member(member) + .role(role) + .build(); + + // then + assertThat(comment.getPost()).isEqualTo(post); + assertThat(comment.getContent()).isEqualTo(content); + assertThat(comment.getMember()).isEqualTo(member); + assertThat(comment.getRole()).isEqualTo(role); + assertThat(comment.getIsAdopted()).isFalse(); + } + + @Test + @DisplayName("댓글 생성 시 isAdopted는 기본적으로 false") + void createComment_defaultIsAdoptedFalse() { + // given + Member member = MemberFixture.createDefault(); + Post post = createDefaultPost(member); + + // when + PostComment comment = PostComment.builder() + .post(post) + .content("테스트 댓글") + .member(member) + .role(member.getRole().name()) + .build(); + + // then + assertThat(comment.getIsAdopted()).isFalse(); + } + } + + @Nested + @DisplayName("댓글 내용 업데이트 테스트") + class UpdateContentTest { + + @Test + @DisplayName("정상적인 댓글 내용 업데이트") + void updateContent_success() { + // given + Member member = MemberFixture.createDefault(); + PostComment comment = createDefaultComment(member); + String newContent = "수정된 댓글 내용"; + + // when + comment.updateContent(newContent); + + // then + assertThat(comment.getContent()).isEqualTo(newContent); + } + + @Test + @DisplayName("null 내용으로 업데이트 시 예외 발생") + void updateContent_withNull_throwsException() { + // given + Member member = MemberFixture.createDefault(); + PostComment comment = createDefaultComment(member); + + // when & then + assertThatThrownBy(() -> comment.updateContent(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("댓글을 입력해주세요"); + } + + @Test + @DisplayName("빈 문자열로 업데이트 시 예외 발생") + void updateContent_withEmpty_throwsException() { + // given + Member member = MemberFixture.createDefault(); + PostComment comment = createDefaultComment(member); + + // when & then + assertThatThrownBy(() -> comment.updateContent("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("댓글을 입력해주세요"); + } + + @Test + @DisplayName("공백만 있는 문자열로 업데이트 시 예외 발생") + void updateContent_withWhitespace_throwsException() { + // given + Member member = MemberFixture.createDefault(); + PostComment comment = createDefaultComment(member); + + // when & then + assertThatThrownBy(() -> comment.updateContent(" ")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("댓글을 입력해주세요"); + } + } + + @Nested + @DisplayName("게시글 업데이트 테스트") + class UpdatePostTest { + + @Test + @DisplayName("댓글의 게시글 업데이트 성공") + void updatePost_success() { + // given + Member member = MemberFixture.createDefault(); + PostComment comment = createDefaultComment(member); + Post newPost = createDefaultPost(member); + + // when + comment.updatePost(newPost); + + // then + assertThat(comment.getPost()).isEqualTo(newPost); + } + + @Test + @DisplayName("댓글의 게시글을 null로 업데이트") + void updatePost_withNull() { + // given + Member member = MemberFixture.createDefault(); + PostComment comment = createDefaultComment(member); + + // when + comment.updatePost(null); + + // then + assertThat(comment.getPost()).isNull(); + } + } + + @Nested + @DisplayName("댓글 채택 테스트") + class AdoptCommentTest { + + @Test + @DisplayName("댓글 채택 성공") + void adoptComment_success() { + // given + Member member = MemberFixture.createDefault(); + PostComment comment = createDefaultComment(member); + + // when + comment.adoptComment(); + + // then + assertThat(comment.getIsAdopted()).isTrue(); + } + + @Test + @DisplayName("이미 채택된 댓글도 채택 가능") + void adoptComment_alreadyAdopted() { + // given + Member member = MemberFixture.createDefault(); + PostComment comment = createDefaultComment(member); + comment.adoptComment(); // 먼저 채택 + + // when + comment.adoptComment(); // 다시 채택 + + // then + assertThat(comment.getIsAdopted()).isTrue(); + } + } + + @Nested + @DisplayName("작성자 확인 테스트") + class AuthorCheckTest { + + @Test + @DisplayName("댓글 작성자 확인 성공") + void isAuthor_success() { + // given + Member author = MemberFixture.create(1L, "author@test.com", "Author", "password", Member.Role.MENTEE); + PostComment comment = createDefaultComment(author); + + // when + Boolean isAuthor = comment.isAuthor(author); + + // then + assertThat(isAuthor).isTrue(); + } + + @Test + @DisplayName("다른 사용자는 댓글 작성자가 아님") + void isAuthor_differentUser_false() { + // given + Member author = MemberFixture.create(1L, "author@test.com", "Author", "password", Member.Role.MENTEE); + Member otherUser = MemberFixture.create(2L, "other@test.com", "Other", "password", Member.Role.MENTEE); + PostComment comment = createDefaultComment(author); + + // when + Boolean isAuthor = comment.isAuthor(otherUser); + + // then + assertThat(isAuthor).isFalse(); + } + + @Test + @DisplayName("댓글 작성자 이름 반환") + void getAuthorName_success() { + // given + Member author = MemberFixture.create(1L, "author@test.com", "Author Name", "password", Member.Role.MENTEE); + PostComment comment = createDefaultComment(author); + + // when + String authorName = comment.getAuthorName(); + + // then + assertThat(authorName).isEqualTo("Author Name"); + } + } + + @Nested + @DisplayName("역할(Role) 테스트") + class RoleTest { + + @Test + @DisplayName("멘티 역할로 댓글 생성") + void createComment_withMenteeRole() { + // given + Member mentee = MemberFixture.create(1L, "mentee@test.com", "Mentee", "password", Member.Role.MENTEE); + Post post = createDefaultPost(mentee); + + // when + PostComment comment = PostComment.builder() + .post(post) + .content("멘티 댓글") + .member(mentee) + .role(mentee.getRole().name()) + .build(); + + // then + assertThat(comment.getRole()).isEqualTo("MENTEE"); + } + + @Test + @DisplayName("멘토 역할로 댓글 생성") + void createComment_withMentorRole() { + // given + Member mentor = MemberFixture.create(1L, "mentor@test.com", "Mentor", "password", Member.Role.MENTOR); + Post post = createDefaultPost(mentor); + + // when + PostComment comment = PostComment.builder() + .post(post) + .content("멘토 댓글") + .member(mentor) + .role(mentor.getRole().name()) + .build(); + + // then + assertThat(comment.getRole()).isEqualTo("MENTOR"); + } + + @Test + @DisplayName("관리자 역할로 댓글 생성") + void createComment_withAdminRole() { + // given + Member admin = MemberFixture.create(1L, "admin@test.com", "Admin", "password", Member.Role.ADMIN); + Post post = createDefaultPost(admin); + + // when + PostComment comment = PostComment.builder() + .post(post) + .content("관리자 댓글") + .member(admin) + .role(admin.getRole().name()) + .build(); + + // then + assertThat(comment.getRole()).isEqualTo("ADMIN"); + } + } + + private Post createDefaultPost(Member member) { + return Post.builder() + .title("테스트 게시글") + .content("테스트 내용") + .member(member) + .postType(Post.PostType.INFORMATIONPOST) + .build(); + } + + private PostComment createDefaultComment(Member member) { + Post post = createDefaultPost(member); + return PostComment.builder() + .post(post) + .content("테스트 댓글") + .member(member) + .role(member.getRole().name()) + .build(); + } +} \ No newline at end of file diff --git a/back/src/test/java/com/back/domain/post/comment/service/PostCommentServiceTest.java b/back/src/test/java/com/back/domain/post/comment/service/PostCommentServiceTest.java new file mode 100644 index 00000000..67df8bfe --- /dev/null +++ b/back/src/test/java/com/back/domain/post/comment/service/PostCommentServiceTest.java @@ -0,0 +1,474 @@ +package com.back.domain.post.comment.service; + +import com.back.domain.member.member.entity.Member; +import com.back.domain.post.comment.dto.CommentAllResponse; +import com.back.domain.post.comment.dto.CommentCreateRequest; +import com.back.domain.post.comment.dto.CommentDeleteRequest; +import com.back.domain.post.comment.dto.CommentModifyRequest; +import com.back.domain.post.comment.entity.PostComment; +import com.back.domain.post.comment.repository.PostCommentRepository; +import com.back.domain.post.post.entity.Post; +import com.back.domain.post.post.repository.PostRepository; +import com.back.fixture.MemberFixture; +import com.back.global.exception.ServiceException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +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.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class PostCommentServiceTest { + + @Mock + private PostRepository postRepository; + + @Mock + private PostCommentRepository postCommentRepository; + + @InjectMocks + private PostCommentService postCommentService; + + @Nested + @DisplayName("댓글 생성 테스트") + class CreateCommentTest { + + @Test + @DisplayName("댓글 생성 성공") + void createComment_success() { + // given + Member member = MemberFixture.create(1L, "user@test.com", "User", "password", Member.Role.MENTEE); + Post post = createDefaultPost(member); + Long postId = 1L; + CommentCreateRequest request = new CommentCreateRequest(); + request.setComment("테스트 댓글"); + + when(postRepository.findById(postId)).thenReturn(Optional.of(post)); + when(postCommentRepository.save(any(PostComment.class))).thenReturn(any(PostComment.class)); + + // when + postCommentService.createComment(member, postId, request); + + // then + verify(postRepository).findById(postId); + verify(postCommentRepository).save(any(PostComment.class)); + } + + @Test + @DisplayName("존재하지 않는 게시글에 댓글 생성 시 실패") + void createComment_postNotExists_failure() { + // given + Member member = MemberFixture.createDefault(); + Long postId = 999L; + CommentCreateRequest request = new CommentCreateRequest(); + request.setComment("테스트 댓글"); + + when(postRepository.findById(postId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> postCommentService.createComment(member, postId, request)) + .isInstanceOf(ServiceException.class) + .hasMessage("400 : 해당 Id의 게시글이 없습니다."); + + verify(postCommentRepository, never()).save(any(PostComment.class)); + } + } + + @Nested + @DisplayName("댓글 조회 테스트") + class GetCommentTest { + + @Test + @DisplayName("게시글의 모든 댓글 조회 성공") + void getAllPostCommentResponse_success() { + // given + Member member1 = MemberFixture.create(1L, "user1@test.com", "User1", "password", Member.Role.MENTEE); + Member member2 = MemberFixture.create(2L, "user2@test.com", "User2", "password", Member.Role.MENTOR); + Post post = createDefaultPost(member1); + Long postId = 1L; + + List comments = Arrays.asList( + createComment(member1, post, "첫 번째 댓글"), + createComment(member2, post, "두 번째 댓글") + ); + + when(postRepository.existsById(postId)).thenReturn(true); + when(postCommentRepository.findCommentsWithMemberByPostId(postId)).thenReturn(comments); + + // when + List result = postCommentService.getAllPostCommentResponse(postId); + + // then + assertThat(result).hasSize(2); + verify(postRepository).existsById(postId); + verify(postCommentRepository).findCommentsWithMemberByPostId(postId); + } + + @Test + @DisplayName("존재하지 않는 게시글의 댓글 조회 시 실패") + void getAllPostCommentResponse_postNotExists_failure() { + // given + Long postId = 999L; + + when(postRepository.existsById(postId)).thenReturn(false); + + // when & then + assertThatThrownBy(() -> postCommentService.getAllPostCommentResponse(postId)) + .isInstanceOf(ServiceException.class) + .hasMessage("400 : 해당 Id의 게시글이 없습니다."); + + verify(postCommentRepository, never()).findCommentsWithMemberByPostId(anyLong()); + } + } + + @Nested + @DisplayName("댓글 삭제 테스트") + class RemoveCommentTest { + + @Test + @DisplayName("댓글 작성자가 댓글 삭제 성공") + void removePostComment_author_success() { + // given + Member author = MemberFixture.create(1L, "author@test.com", "Author", "password", Member.Role.MENTEE); + Post post = createDefaultPost(author); + PostComment comment = createComment(author, post, "삭제할 댓글"); + Long postId = 1L; + Long commentId = 1L; + CommentDeleteRequest request = new CommentDeleteRequest(); + request.setCommentId(commentId); + + when(postRepository.existsById(postId)).thenReturn(true); + when(postCommentRepository.findById(commentId)).thenReturn(Optional.of(comment)); + + // when + postCommentService.removePostComment(postId, request, author); + + // then + verify(postCommentRepository).delete(comment); + } + + @Test + @DisplayName("댓글 작성자가 아닌 사용자가 댓글 삭제 시도 시 실패") + void removePostComment_notAuthor_failure() { + // given + Member author = MemberFixture.create(1L, "author@test.com", "Author", "password", Member.Role.MENTEE); + Member otherUser = MemberFixture.create(2L, "other@test.com", "Other", "password", Member.Role.MENTEE); + Post post = createDefaultPost(author); + PostComment comment = createComment(author, post, "삭제할 댓글"); + Long postId = 1L; + Long commentId = 1L; + CommentDeleteRequest request = new CommentDeleteRequest(); + request.setCommentId(commentId); + + when(postRepository.existsById(postId)).thenReturn(true); + when(postCommentRepository.findById(commentId)).thenReturn(Optional.of(comment)); + + // when & then + assertThatThrownBy(() -> postCommentService.removePostComment(postId, request, otherUser)) + .isInstanceOf(ServiceException.class) + .hasMessage("400 : 삭제 권한이 없습니다."); + + verify(postCommentRepository, never()).delete(any(PostComment.class)); + } + + @Test + @DisplayName("존재하지 않는 댓글 삭제 시도 시 실패") + void removePostComment_commentNotExists_failure() { + // given + Member member = MemberFixture.createDefault(); + Long postId = 1L; + Long commentId = 999L; + CommentDeleteRequest request = new CommentDeleteRequest(); + request.setCommentId(commentId); + + when(postRepository.existsById(postId)).thenReturn(true); + when(postCommentRepository.findById(commentId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> postCommentService.removePostComment(postId, request, member)) + .isInstanceOf(ServiceException.class) + .hasMessage("400 : 해당 Id의 댓글이 없습니다."); + + verify(postCommentRepository, never()).delete(any(PostComment.class)); + } + } + + @Nested + @DisplayName("댓글 수정 테스트") + class UpdateCommentTest { + + @Test + @DisplayName("댓글 작성자가 댓글 수정 성공") + void updatePostComment_author_success() { + // given + Member author = MemberFixture.create(1L, "author@test.com", "Author", "password", Member.Role.MENTEE); + Post post = createDefaultPost(author); + PostComment comment = createComment(author, post, "원본 댓글"); + Long postId = 1L; + Long commentId = 1L; + CommentModifyRequest request = new CommentModifyRequest(); + request.setCommentId(commentId); + request.setContent("수정된 댓글"); + + when(postRepository.existsById(postId)).thenReturn(true); + when(postCommentRepository.findById(commentId)).thenReturn(Optional.of(comment)); + + // when + postCommentService.updatePostComment(postId, request, author); + + // then + verify(postCommentRepository).findById(commentId); + } + + @Test + @DisplayName("댓글 작성자가 아닌 사용자가 댓글 수정 시도 시 실패") + void updatePostComment_notAuthor_failure() { + // given + Member author = MemberFixture.create(1L, "author@test.com", "Author", "password", Member.Role.MENTEE); + Member otherUser = MemberFixture.create(2L, "other@test.com", "Other", "password", Member.Role.MENTEE); + Post post = createDefaultPost(author); + PostComment comment = createComment(author, post, "원본 댓글"); + Long postId = 1L; + Long commentId = 1L; + + CommentModifyRequest request = new CommentModifyRequest(); + request.setCommentId(commentId); + request.setContent("400 : 수정된 댓글"); + + when(postRepository.existsById(postId)).thenReturn(true); + when(postCommentRepository.findById(commentId)).thenReturn(Optional.of(comment)); + + // when & then + assertThatThrownBy(() -> postCommentService.updatePostComment(postId, request, otherUser)) + .isInstanceOf(ServiceException.class) + .hasMessage("400 : 수정 권한이 없습니다."); + } + + @Test + @DisplayName("빈 내용으로 댓글 수정 시도 시 실패") + void updatePostComment_emptyContent_failure() { + // given + Member author = MemberFixture.create(1L, "author@test.com", "Author", "password", Member.Role.MENTEE); + Post post = createDefaultPost(author); + PostComment comment = createComment(author, post, "원본 댓글"); + Long postId = 1L; + Long commentId = 1L; + + CommentModifyRequest request = new CommentModifyRequest(); + request.setCommentId(commentId); + request.setContent(""); + + when(postRepository.existsById(postId)).thenReturn(true); + when(postCommentRepository.findById(commentId)).thenReturn(Optional.of(comment)); + + // when & then + assertThatThrownBy(() -> postCommentService.updatePostComment(postId, request, author)) + .isInstanceOf(ServiceException.class) + .hasMessage("400 : 댓글은 비어 있을 수 없습니다."); + } + + @Test + @DisplayName("null 내용으로 댓글 수정 시도 시 실패") + void updatePostComment_nullContent_failure() { + // given + Member author = MemberFixture.create(1L, "author@test.com", "Author", "password", Member.Role.MENTEE); + Post post = createDefaultPost(author); + PostComment comment = createComment(author, post, "원본 댓글"); + Long postId = 1L; + Long commentId = 1L; + CommentModifyRequest request = new CommentModifyRequest(); + request.setCommentId(commentId); + request.setContent(null); + + when(postRepository.existsById(postId)).thenReturn(true); + when(postCommentRepository.findById(commentId)).thenReturn(Optional.of(comment)); + + // when & then + assertThatThrownBy(() -> postCommentService.updatePostComment(postId, request, author)) + .isInstanceOf(ServiceException.class) + .hasMessage("400 : 댓글은 비어 있을 수 없습니다."); + } + } + + @Nested + @DisplayName("댓글 채택 테스트") + class AdoptCommentTest { + + @Test + @DisplayName("질문 게시글 작성자가 댓글 채택 성공") + void adoptComment_questionPostAuthor_success() { + // given + Member postAuthor = MemberFixture.create(1L, "author@test.com", "Author", "password", Member.Role.MENTEE); + Member commenter = MemberFixture.create(2L, "commenter@test.com", "Commenter", "password", Member.Role.MENTOR); + Post questionPost = createQuestionPost(postAuthor); + PostComment comment = createComment(commenter, questionPost, "답변 댓글"); + Long commentId = 1L; + + when(postCommentRepository.findById(commentId)).thenReturn(Optional.of(comment)); + when(postCommentRepository.existsByPostAndIsAdoptedTrue(questionPost)).thenReturn(false); + + // when + postCommentService.adoptComment(commentId, postAuthor); + + // then + verify(postCommentRepository).findById(commentId); + verify(postCommentRepository).existsByPostAndIsAdoptedTrue(questionPost); + } + + @Test + @DisplayName("게시글 작성자가 아닌 사용자가 댓글 채택 시도 시 실패") + void adoptComment_notPostAuthor_failure() { + // given + Member postAuthor = MemberFixture.create(1L, "author@test.com", "Author", "password", Member.Role.MENTEE); + Member commenter = MemberFixture.create(2L, "commenter@test.com", "Commenter", "password", Member.Role.MENTOR); + Member otherUser = MemberFixture.create(3L, "other@test.com", "Other", "password", Member.Role.MENTEE); + Post questionPost = createQuestionPost(postAuthor); + PostComment comment = createComment(commenter, questionPost, "답변 댓글"); + Long commentId = 1L; + + when(postCommentRepository.findById(commentId)).thenReturn(Optional.of(comment)); + + // when & then + assertThatThrownBy(() -> postCommentService.adoptComment(commentId, otherUser)) + .isInstanceOf(ServiceException.class) + .hasMessage("400 : 채택 권한이 없습니다."); + } + + @Test + @DisplayName("질문 게시글이 아닌 게시글의 댓글 채택 시도 시 실패") + void adoptComment_notQuestionPost_failure() { + // given + Member postAuthor = MemberFixture.create(1L, "author@test.com", "Author", "password", Member.Role.MENTEE); + Member commenter = MemberFixture.create(2L, "commenter@test.com", "Commenter", "password", Member.Role.MENTOR); + Post informationPost = createDefaultPost(postAuthor); + PostComment comment = createComment(commenter, informationPost, "일반 댓글"); + Long commentId = 1L; + + when(postCommentRepository.findById(commentId)).thenReturn(Optional.of(comment)); + + // when & then + assertThatThrownBy(() -> postCommentService.adoptComment(commentId, postAuthor)) + .isInstanceOf(ServiceException.class) + .hasMessage("400 : 질문 게시글에만 댓글 채택이 가능합니다."); + } + + @Test + @DisplayName("이미 채택된 댓글을 다시 채택 시도 시 실패") + void adoptComment_alreadyAdopted_failure() { + // given + Member postAuthor = MemberFixture.create(1L, "author@test.com", "Author", "password", Member.Role.MENTEE); + Member commenter = MemberFixture.create(2L, "commenter@test.com", "Commenter", "password", Member.Role.MENTOR); + Post questionPost = createQuestionPost(postAuthor); + PostComment comment = createComment(commenter, questionPost, "답변 댓글"); + comment.adoptComment(); // 이미 채택된 상태 + Long commentId = 1L; + + when(postCommentRepository.findById(commentId)).thenReturn(Optional.of(comment)); + + // when & then + assertThatThrownBy(() -> postCommentService.adoptComment(commentId, postAuthor)) + .isInstanceOf(ServiceException.class) + .hasMessage("400 : 이미 채택된 댓글입니다."); + } + + @Test + @DisplayName("이미 다른 댓글이 채택된 게시글에서 댓글 채택 시도 시 실패") + void adoptComment_anotherCommentAlreadyAdopted_failure() { + // given + Member postAuthor = MemberFixture.create(1L, "author@test.com", "Author", "password", Member.Role.MENTEE); + Member commenter = MemberFixture.create(2L, "commenter@test.com", "Commenter", "password", Member.Role.MENTOR); + Post questionPost = createQuestionPost(postAuthor); + PostComment comment = createComment(commenter, questionPost, "답변 댓글"); + Long commentId = 1L; + + when(postCommentRepository.findById(commentId)).thenReturn(Optional.of(comment)); + when(postCommentRepository.existsByPostAndIsAdoptedTrue(questionPost)).thenReturn(true); + + // when & then + assertThatThrownBy(() -> postCommentService.adoptComment(commentId, postAuthor)) + .isInstanceOf(ServiceException.class) + .hasMessage("400 : 이미 채택된 댓글이 있습니다."); + } + + @Test + @DisplayName("존재하지 않는 댓글 채택 시도 시 실패") + void adoptComment_commentNotExists_failure() { + // given + Member member = MemberFixture.createDefault(); + Long commentId = 999L; + + when(postCommentRepository.findById(commentId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> postCommentService.adoptComment(commentId, member)) + .isInstanceOf(ServiceException.class) + .hasMessage("400 : 해당 Id의 댓글이 없습니다."); + } + } + + @Nested + @DisplayName("게시글 존재 검증 테스트") + class ValidatePostExistsTest { + + @Test + @DisplayName("null 게시글 ID 검증 실패") + void validatePostExists_nullId_failure() { + // given + Long postId = null; + + // when & then + assertThatThrownBy(() -> postCommentService.getAllPostCommentResponse(postId)) + .isInstanceOf(ServiceException.class) + .hasMessage("400 : 유효하지 않은 게시글 Id입니다."); + } + + @Test + @DisplayName("0 이하의 게시글 ID 검증 실패") + void validatePostExists_invalidId_failure() { + // given + Long postId = 0L; + + // when & then + assertThatThrownBy(() -> postCommentService.getAllPostCommentResponse(postId)) + .isInstanceOf(ServiceException.class) + .hasMessage("400 : 유효하지 않은 게시글 Id입니다."); + } + } + + private Post createDefaultPost(Member member) { + return Post.builder() + .title("테스트 게시글") + .content("테스트 내용") + .member(member) + .postType(Post.PostType.INFORMATIONPOST) + .build(); + } + + private Post createQuestionPost(Member member) { + return Post.builder() + .title("질문 게시글") + .content("질문 내용") + .member(member) + .postType(Post.PostType.QUESTIONPOST) + .build(); + } + + private PostComment createComment(Member member, Post post, String content) { + return PostComment.builder() + .post(post) + .content(content) + .member(member) + .role(member.getRole().name()) + .build(); + } +} diff --git a/back/src/test/java/com/back/domain/post/post/controller/InformationPostControllerTest.java b/back/src/test/java/com/back/domain/post/post/controller/PostControllerTest.java similarity index 85% rename from back/src/test/java/com/back/domain/post/post/controller/InformationPostControllerTest.java rename to back/src/test/java/com/back/domain/post/post/controller/PostControllerTest.java index 7f21c119..f96b3d8c 100644 --- a/back/src/test/java/com/back/domain/post/post/controller/InformationPostControllerTest.java +++ b/back/src/test/java/com/back/domain/post/post/controller/PostControllerTest.java @@ -33,7 +33,7 @@ @SpringBootTest @AutoConfigureMockMvc @Transactional -public class InformationPostControllerTest { +public class PostControllerTest { @Autowired private PostService postService; @@ -75,7 +75,7 @@ void setUp() { void t1() throws Exception { ResultActions resultActions = mvc .perform( - post("/post/infor") + post("/post") .contentType(MediaType.APPLICATION_JSON) .content(""" { @@ -92,7 +92,7 @@ void t1() throws Exception { Post createdPost = postService.findById(4L); resultActions - .andExpect(handler().handlerType(InformationPostController.class)) + .andExpect(handler().handlerType(PostController.class)) .andExpect(handler().methodName("createPost")) .andExpect(status().isOk()) .andExpect(jsonPath("$.msg").value("게시글이 성공적으로 생성되었습니다.")) @@ -106,7 +106,7 @@ void t1() throws Exception { void t6() throws Exception { ResultActions resultActions = mvc .perform( - post("/post/infor") + post("/post") .contentType(MediaType.APPLICATION_JSON) .content(""" { @@ -121,7 +121,7 @@ void t6() throws Exception { resultActions - .andExpect(handler().handlerType(InformationPostController.class)) + .andExpect(handler().handlerType(PostController.class)) .andExpect(handler().methodName("createPost")) .andExpect(status().isBadRequest()) @@ -133,7 +133,7 @@ void t6() throws Exception { void t2() throws Exception { ResultActions resultActions = mvc .perform( - post("/post/infor") + post("/post") .contentType(MediaType.APPLICATION_JSON) .content(""" { @@ -147,10 +147,10 @@ void t2() throws Exception { .andDo(print()); resultActions - .andExpect(handler().handlerType(InformationPostController.class)) + .andExpect(handler().handlerType(PostController.class)) .andExpect(handler().methodName("createPost")) .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.resultCode").value("400-2")) + .andExpect(jsonPath("$.resultCode").value("400")) .andExpect(jsonPath("$.msg").value("유효하지 않은 PostType입니다.")); } @@ -159,7 +159,7 @@ void t2() throws Exception { void t3() throws Exception { // 테스트용 게시글 먼저 생성 mvc.perform( - post("/post/infor") + post("/post") .contentType(MediaType.APPLICATION_JSON) .content(""" { @@ -172,7 +172,7 @@ void t3() throws Exception { ); mvc.perform( - post("/post/infor") + post("/post") .contentType(MediaType.APPLICATION_JSON) .content(""" { @@ -187,12 +187,12 @@ void t3() throws Exception { // 페이징 조회 테스트 - 기본값 ResultActions resultActions = mvc .perform( - get("/post/infor") + get("/post/page/{postType}", "INFORMATIONPOST") ) .andDo(print()); resultActions - .andExpect(handler().handlerType(InformationPostController.class)) + .andExpect(handler().handlerType(PostController.class)) .andExpect(handler().methodName("getPostWithPage")) .andExpect(status().isOk()) .andExpect(jsonPath("$.data").exists()) @@ -207,14 +207,14 @@ void t3() throws Exception { void t3_1() throws Exception { ResultActions resultActions = mvc .perform( - get("/post/infor") + get("/post/page/{postType}", "INFORMATIONPOST") .param("page", "0") .param("size", "5") ) .andDo(print()); resultActions - .andExpect(handler().handlerType(InformationPostController.class)) + .andExpect(handler().handlerType(PostController.class)) .andExpect(handler().methodName("getPostWithPage")) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.posts").isArray()) @@ -228,7 +228,7 @@ void t3_1() throws Exception { void t3_2() throws Exception { // 검색 대상 게시글 생성 mvc.perform( - post("/post/infor") + post("/post") .contentType(MediaType.APPLICATION_JSON) .content(""" { @@ -242,7 +242,7 @@ void t3_2() throws Exception { ResultActions resultActions = mvc .perform( - get("/post/infor") + get("/post/page/{postType}", "INFORMATIONPOST") .param("keyword", "Spring") .param("page", "0") .param("size", "10") @@ -250,7 +250,7 @@ void t3_2() throws Exception { .andDo(print()); resultActions - .andExpect(handler().handlerType(InformationPostController.class)) + .andExpect(handler().handlerType(PostController.class)) .andExpect(handler().methodName("getPostWithPage")) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.posts").isArray()) @@ -262,13 +262,13 @@ void t3_2() throws Exception { void t4() throws Exception { ResultActions resultActions = mvc .perform( - get("/post/infor/{post_id}", 1L) + get("/post/{post_id}", 1L) ) .andDo(print()); resultActions - .andExpect(handler().handlerType(InformationPostController.class)) + .andExpect(handler().handlerType(PostController.class)) .andExpect(handler().methodName("getSinglePost")) .andExpect(status().isOk()) .andExpect(jsonPath("$.msg").value("게시글 단건 조회 성공")) @@ -282,12 +282,12 @@ void t4() throws Exception { void t5() throws Exception { ResultActions resultActions = mvc .perform( - get("/post/infor/{post_id}", 999L) + get("/post/{post_id}", 999L) ) .andDo(print()); resultActions - .andExpect(handler().handlerType(InformationPostController.class)) + .andExpect(handler().handlerType(PostController.class)) .andExpect(handler().methodName("getSinglePost")) .andExpect(jsonPath("$.resultCode").value("400")) .andExpect(jsonPath("$.msg").value("해당 Id의 게시글이 없습니다.")); @@ -297,7 +297,7 @@ void t5() throws Exception { @DisplayName("게시글 삭제") void t7() throws Exception { mvc.perform( - post("/post/infor") + post("/post") .contentType(MediaType.APPLICATION_JSON) .content(""" { @@ -312,12 +312,12 @@ void t7() throws Exception { ResultActions resultActions = mvc .perform( - delete("/post/infor/{post_id}", 7L) + delete("/post/{post_id}", 7L) ) .andDo(print()); resultActions - .andExpect(handler().handlerType(InformationPostController.class)) + .andExpect(handler().handlerType(PostController.class)) .andExpect(handler().methodName("removePost")) .andExpect(jsonPath("$.resultCode").value("200")) .andExpect(jsonPath("$.msg").value("게시글 삭제 성공")); @@ -328,12 +328,12 @@ void t7() throws Exception { void t8() throws Exception { ResultActions resultActions = mvc .perform( - delete("/post/infor/{post_id}", 3L) + delete("/post/{post_id}", 3L) ) .andDo(print()); resultActions - .andExpect(handler().handlerType(InformationPostController.class)) + .andExpect(handler().handlerType(PostController.class)) .andExpect(handler().methodName("removePost")) .andExpect(jsonPath("$.resultCode").value("400")) .andExpect(jsonPath("$.msg").value("삭제 권한이 없습니다.")); @@ -343,7 +343,7 @@ void t8() throws Exception { @DisplayName("게시글 수정") void t9() throws Exception { mvc.perform( - post("/post/infor") + post("/post") .contentType(MediaType.APPLICATION_JSON) .content(""" { @@ -358,7 +358,7 @@ void t9() throws Exception { ResultActions resultActions = mvc .perform( - put("/post/infor/{post_id}", 8L) + put("/post/{post_id}", 8L) .contentType(MediaType.APPLICATION_JSON) .content(""" { @@ -370,7 +370,7 @@ void t9() throws Exception { .andDo(print()); resultActions - .andExpect(handler().handlerType(InformationPostController.class)) + .andExpect(handler().handlerType(PostController.class)) .andExpect(handler().methodName("updatePost")) .andExpect(jsonPath("$.resultCode").value("200")) .andExpect(jsonPath("$.msg").value("게시글 수정 성공")); @@ -381,7 +381,7 @@ void t9() throws Exception { void t10() throws Exception { ResultActions resultActions = mvc .perform( - put("/post/infor/{post_id}", 2L) + put("/post/{post_id}", 2L) .contentType(MediaType.APPLICATION_JSON) .content(""" { @@ -393,7 +393,7 @@ void t10() throws Exception { .andDo(print()); resultActions - .andExpect(handler().handlerType(InformationPostController.class)) + .andExpect(handler().handlerType(PostController.class)) .andExpect(handler().methodName("updatePost")) .andExpect(jsonPath("$.resultCode").value("400")) .andExpect(jsonPath("$.msg").value("수정 권한이 없습니다.")); @@ -404,7 +404,7 @@ void t10() throws Exception { void t11() throws Exception { ResultActions resultActions = mvc .perform( - put("/post/infor/{post_id}", 6L) + put("/post/{post_id}", 6L) .contentType(MediaType.APPLICATION_JSON) .content(""" { @@ -416,7 +416,7 @@ void t11() throws Exception { .andDo(print()); resultActions - .andExpect(handler().handlerType(InformationPostController.class)) + .andExpect(handler().handlerType(PostController.class)) .andExpect(handler().methodName("updatePost")) .andExpect(jsonPath("$.resultCode").value("400-1")) .andExpect(jsonPath("$.msg").value("title-NotBlank-제목은 null 혹은 공백일 수 없습니다.")); @@ -427,12 +427,12 @@ void t11() throws Exception { void t12() throws Exception { ResultActions resultActions = mvc .perform( - post("/post/infor/{post_id}/liked", 1L) + post("/post/{post_id}/liked", 1L) ) .andDo(print()); resultActions - .andExpect(handler().handlerType(InformationPostController.class)) + .andExpect(handler().handlerType(PostController.class)) .andExpect(handler().methodName("likePost")) .andExpect(jsonPath("$.resultCode").value("200")) .andExpect(jsonPath("$.msg").value("게시글 좋아요 성공")); @@ -443,12 +443,12 @@ void t12() throws Exception { void t13() throws Exception { ResultActions resultActions = mvc .perform( - get("/post/infor/{post_id}/liked", 1L) + get("/post/{post_id}/liked", 1L) ) .andDo(print()); resultActions - .andExpect(handler().handlerType(InformationPostController.class)) + .andExpect(handler().handlerType(PostController.class)) .andExpect(handler().methodName("getLike")) .andExpect(jsonPath("$.resultCode").value("200")) .andExpect(jsonPath("$.msg").value("게시글 좋아요 조회 성공")) @@ -460,12 +460,12 @@ void t13() throws Exception { void t14() throws Exception { ResultActions resultActions = mvc .perform( - post("/post/infor/{post_id}/disliked", 1L) + post("/post/{post_id}/disliked", 1L) ) .andDo(print()); resultActions - .andExpect(handler().handlerType(InformationPostController.class)) + .andExpect(handler().handlerType(PostController.class)) .andExpect(handler().methodName("disLikePost")) .andExpect(jsonPath("$.resultCode").value("200")) .andExpect(jsonPath("$.msg").value("게시글 싫어요 성공")); @@ -476,12 +476,12 @@ void t14() throws Exception { void t15() throws Exception { ResultActions resultActions = mvc .perform( - get("/post/infor/{post_id}/Disliked", 1L) + get("/post/{post_id}/disliked", 1L) ) .andDo(print()); resultActions - .andExpect(handler().handlerType(InformationPostController.class)) + .andExpect(handler().handlerType(PostController.class)) .andExpect(handler().methodName("getDisLike")) .andExpect(jsonPath("$.resultCode").value("200")) .andExpect(jsonPath("$.msg").value("게시글 싫어요 조회 성공")) @@ -492,18 +492,18 @@ void t15() throws Exception { @DisplayName("좋아요 -> 싫어요 토글 테스트") void t16() throws Exception { // 먼저 좋아요 - mvc.perform(post("/post/infor/{post_id}/liked", 1L)) + mvc.perform(post("/post/{post_id}/liked", 1L)) .andExpect(jsonPath("$.msg").value("게시글 좋아요 성공")); // 싫어요로 변경 ResultActions resultActions = mvc .perform( - post("/post/infor/{post_id}/disliked", 1L) + post("/post/{post_id}/disliked", 1L) ) .andDo(print()); resultActions - .andExpect(handler().handlerType(InformationPostController.class)) + .andExpect(handler().handlerType(PostController.class)) .andExpect(handler().methodName("disLikePost")) .andExpect(jsonPath("$.resultCode").value("200")) .andExpect(jsonPath("$.msg").value("게시글 싫어요 성공")); @@ -513,18 +513,18 @@ void t16() throws Exception { @DisplayName("좋아요 중복 클릭 - 좋아요 취소") void t17() throws Exception { // 첫 번째 좋아요 - mvc.perform(post("/post/infor/{post_id}/liked", 1L)) + mvc.perform(post("/post/{post_id}/liked", 1L)) .andExpect(jsonPath("$.msg").value("게시글 좋아요 성공")); // 두 번째 좋아요 (취소) ResultActions resultActions = mvc .perform( - post("/post/infor/{post_id}/liked", 1L) + post("/post/{post_id}/liked", 1L) ) .andDo(print()); resultActions - .andExpect(handler().handlerType(InformationPostController.class)) + .andExpect(handler().handlerType(PostController.class)) .andExpect(handler().methodName("likePost")) .andExpect(jsonPath("$.resultCode").value("200")) .andExpect(jsonPath("$.msg").value("게시글 좋아요 성공")); @@ -535,16 +535,16 @@ void t17() throws Exception { void t18() throws Exception { // 좋아요 추가하여 좋아요 정보도 함께 조회되는지 확인 - mvc.perform(post("/post/infor/{post_id}/liked", 1L)); + mvc.perform(post("/post/{post_id}/liked", 1L)); ResultActions resultActions = mvc .perform( - get("/post/infor/Detail/{post_id}", 1L) + get("/post/Detail/{post_id}", 1L) ) .andDo(print()); resultActions - .andExpect(handler().handlerType(InformationPostController.class)) + .andExpect(handler().handlerType(PostController.class)) .andExpect(handler().methodName("getPostDetail")) .andExpect(status().isOk()) .andExpect(jsonPath("$.resultCode").value("200")) @@ -566,12 +566,12 @@ void t18() throws Exception { void t19() throws Exception { ResultActions resultActions = mvc .perform( - get("/post/infor/Detail/{post_id}", 999L) + get("/post/Detail/{post_id}", 999L) ) .andDo(print()); resultActions - .andExpect(handler().handlerType(InformationPostController.class)) + .andExpect(handler().handlerType(PostController.class)) .andExpect(handler().methodName("getPostDetail")) .andExpect(jsonPath("$.resultCode").value("400")) .andExpect(jsonPath("$.msg").value("해당 Id의 게시글이 없습니다.")); diff --git a/back/src/test/java/com/back/domain/post/post/entity/PostTest.java b/back/src/test/java/com/back/domain/post/post/entity/PostTest.java new file mode 100644 index 00000000..48c4c258 --- /dev/null +++ b/back/src/test/java/com/back/domain/post/post/entity/PostTest.java @@ -0,0 +1,352 @@ +package com.back.domain.post.post.entity; + +import com.back.domain.member.member.entity.Member; +import com.back.domain.post.comment.entity.PostComment; +import com.back.fixture.MemberFixture; +import com.back.global.exception.ServiceException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +class PostTest { + + @Nested + @DisplayName("Post 생성 테스트") + class CreatePostTest { + + @Test + @DisplayName("정상적인 Post 생성") + void createPost_success() { + // given + Member member = MemberFixture.createDefault(); + String title = "테스트 제목"; + String content = "테스트 내용"; + Post.PostType postType = Post.PostType.INFORMATIONPOST; + + // when + Post post = Post.builder() + .title(title) + .content(content) + .member(member) + .postType(postType) + .build(); + + // then + assertThat(post.getTitle()).isEqualTo(title); + assertThat(post.getContent()).isEqualTo(content); + assertThat(post.getMember()).isEqualTo(member); + assertThat(post.getPostType()).isEqualTo(postType); + assertThat(post.getViewCount()).isEqualTo(0); + assertThat(post.getComments()).isEmpty(); + } + + @Test + @DisplayName("댓글 리스트가 null일 때 빈 리스트로 초기화") + void createPost_withNullComments_initializeEmptyList() { + // given + Member member = MemberFixture.createDefault(); + + // when + Post post = Post.builder() + .title("제목") + .content("내용") + .member(member) + .postType(Post.PostType.INFORMATIONPOST) + .comments(null) + .build(); + + // then + assertThat(post.getComments()).isNotNull(); + assertThat(post.getComments()).isEmpty(); + } + } + + @Nested + @DisplayName("PostType 검증 테스트") + class ValidPostTypeTest { + + @Test + @DisplayName("유효한 PostType 검증 성공") + void validPostType_success() { + // given & when & then + assertThatNoException().isThrownBy(() -> Post.validPostType("INFORMATIONPOST")); + assertThatNoException().isThrownBy(() -> Post.validPostType("PRACTICEPOST")); + assertThatNoException().isThrownBy(() -> Post.validPostType("QUESTIONPOST")); + } + + @Test + @DisplayName("유효하지 않은 PostType 검증 실패") + void validPostType_failure() { + // given + String invalidPostType = "INVALIDPOST"; + + // when & then + assertThatThrownBy(() -> Post.validPostType(invalidPostType)) + .isInstanceOf(ServiceException.class) + .hasMessage("400 : 유효하지 않은 PostType입니다."); + } + } + + @Nested + @DisplayName("게시글 업데이트 테스트") + class UpdatePostTest { + + @Test + @DisplayName("제목 업데이트 성공") + void updateTitle_success() { + // given + Member member = MemberFixture.createDefault(); + Post post = createDefaultPost(member); + String newTitle = "새로운 제목"; + + // when + post.updateTitle(newTitle); + + // then + assertThat(post.getTitle()).isEqualTo(newTitle); + } + + @Test + @DisplayName("null 제목으로 업데이트 실패") + void updateTitle_withNull_failure() { + // given + Member member = MemberFixture.createDefault(); + Post post = createDefaultPost(member); + + // when & then + assertThatThrownBy(() -> post.updateTitle(null)) + .isInstanceOf(ServiceException.class) + .hasMessage("400 : 제목은 null이거나 공백일 수 없습니다."); + } + + @Test + @DisplayName("공백 제목으로 업데이트 실패") + void updateTitle_withBlank_failure() { + // given + Member member = MemberFixture.createDefault(); + Post post = createDefaultPost(member); + + // when & then + assertThatThrownBy(() -> post.updateTitle(" ")) + .isInstanceOf(ServiceException.class) + .hasMessage("400 : 제목은 null이거나 공백일 수 없습니다."); + } + + @Test + @DisplayName("내용 업데이트 성공") + void updateContent_success() { + // given + Member member = MemberFixture.createDefault(); + Post post = createDefaultPost(member); + String newContent = "새로운 내용"; + + // when + post.updateContent(newContent); + + // then + assertThat(post.getContent()).isEqualTo(newContent); + } + + @Test + @DisplayName("null 내용으로 업데이트 실패") + void updateContent_withNull_failure() { + // given + Member member = MemberFixture.createDefault(); + Post post = createDefaultPost(member); + + // when & then + assertThatThrownBy(() -> post.updateContent(null)) + .isInstanceOf(ServiceException.class) + .hasMessage("400 : 내용은 null이거나 공백일 수 없습니다."); + } + + @Test + @DisplayName("공백 내용으로 업데이트 실패") + void updateContent_withBlank_failure() { + // given + Member member = MemberFixture.createDefault(); + Post post = createDefaultPost(member); + + // when & then + assertThatThrownBy(() -> post.updateContent(" ")) + .isInstanceOf(ServiceException.class) + .hasMessage("400 : 내용은 null이거나 공백일 수 없습니다."); + } + } + + @Nested + @DisplayName("댓글 관리 테스트") + class CommentManagementTest { + + @Test + @DisplayName("댓글 추가 성공") + void addComment_success() { + // given + Member member = MemberFixture.createDefault(); + Post post = createDefaultPost(member); + PostComment comment = createComment(member, post); + + // when + post.addComment(comment); + + // then + assertThat(post.getComments()).hasSize(1); + assertThat(post.getComments()).contains(comment); + assertThat(comment.getPost()).isEqualTo(post); + } + + @Test + @DisplayName("댓글 제거 성공") + void removeComment_success() { + // given + Member member = MemberFixture.createDefault(); + Post post = createDefaultPost(member); + PostComment comment = createComment(member, post); + post.addComment(comment); + + // when + post.removeComment(comment); + + // then + assertThat(post.getComments()).isEmpty(); + assertThat(comment.getPost()).isNull(); + } + } + + @Nested + @DisplayName("작성자 확인 테스트") + class AuthorCheckTest { + + @Test + @DisplayName("작성자 확인 성공") + void isAuthor_success() { + // given + Member author = MemberFixture.create(1L, "author@test.com", "Author", "password", Member.Role.MENTEE); + Post post = createDefaultPost(author); + + // when + Boolean isAuthor = post.isAuthor(author); + + // then + assertThat(isAuthor).isTrue(); + } + + @Test + @DisplayName("다른 사용자는 작성자가 아님") + void isAuthor_differentUser_false() { + // given + Member author = MemberFixture.create(1L, "author@test.com", "Author", "password", Member.Role.MENTEE); + Member otherUser = MemberFixture.create(2L, "other@test.com", "Other", "password", Member.Role.MENTEE); + Post post = createDefaultPost(author); + + // when + Boolean isAuthor = post.isAuthor(otherUser); + + // then + assertThat(isAuthor).isFalse(); + } + + @Test + @DisplayName("작성자 이름 반환") + void getAuthorName_success() { + // given + Member author = MemberFixture.create(1L, "author@test.com", "Author Name", "password", Member.Role.MENTEE); + Post post = createDefaultPost(author); + + // when + String authorName = post.getAuthorName(); + + // then + assertThat(authorName).isEqualTo("Author Name"); + } + } + + @Nested + @DisplayName("조회수 증가 테스트") + class ViewCountTest { + + @Test + @DisplayName("조회수 증가 성공") + void increaseViewCount_success() { + // given + Member member = MemberFixture.createDefault(); + Post post = createDefaultPost(member); + int initialViewCount = post.getViewCount(); + + // when + post.increaseViewCount(); + + // then + assertThat(post.getViewCount()).isEqualTo(initialViewCount + 1); + } + + @Test + @DisplayName("조회수 여러 번 증가") + void increaseViewCount_multiple_times() { + // given + Member member = MemberFixture.createDefault(); + Post post = createDefaultPost(member); + + // when + post.increaseViewCount(); + post.increaseViewCount(); + post.increaseViewCount(); + + // then + assertThat(post.getViewCount()).isEqualTo(3); + } + } + + @Nested + @DisplayName("해결 상태 업데이트 테스트") + class ResolveStatusTest { + + @Test + @DisplayName("해결 상태를 true로 업데이트") + void updateResolveStatus_true() { + // given + Member member = MemberFixture.createDefault(); + Post post = createDefaultPost(member); + + // when + post.updateResolveStatus(true); + + // then + assertThat(post.getIsResolve()).isTrue(); + } + + @Test + @DisplayName("해결 상태를 false로 업데이트") + void updateResolveStatus_false() { + // given + Member member = MemberFixture.createDefault(); + Post post = createDefaultPost(member); + + // when + post.updateResolveStatus(false); + + // then + assertThat(post.getIsResolve()).isFalse(); + } + } + + private Post createDefaultPost(Member member) { + return Post.builder() + .title("테스트 제목") + .content("테스트 내용") + .member(member) + .postType(Post.PostType.INFORMATIONPOST) + .build(); + } + + private PostComment createComment(Member member, Post post) { + return PostComment.builder() + .post(post) + .content("테스트 댓글") + .member(member) + .role(member.getRole().name()) + .build(); + } +} diff --git a/back/src/test/java/com/back/domain/post/post/service/PostServiceTest.java b/back/src/test/java/com/back/domain/post/post/service/PostServiceTest.java new file mode 100644 index 00000000..1f2c6456 --- /dev/null +++ b/back/src/test/java/com/back/domain/post/post/service/PostServiceTest.java @@ -0,0 +1,431 @@ +package com.back.domain.post.post.service; + +import com.back.domain.member.member.entity.Member; +import com.back.domain.post.post.dto.PostCreateRequest; +import com.back.domain.post.post.dto.PostDto; +import com.back.domain.post.post.entity.Post; +import com.back.domain.post.post.repository.PostRepository; +import com.back.fixture.MemberFixture; +import com.back.global.exception.ServiceException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +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 org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class PostServiceTest { + + @Mock + private PostRepository postRepository; + + @InjectMocks + private PostService postService; + + @Nested + @DisplayName("게시글 생성 테스트") + class CreatePostTest { + + @Test + @DisplayName("정보 공유 게시글 생성 성공") + void createPost_informationPost_success() { + // given + Member member = MemberFixture.create(1L, "test@test.com", "Test User", "password", Member.Role.MENTEE); + PostCreateRequest request = new PostCreateRequest(); + request.setContent("내용"); + request.setTitle("제목"); + request.setPostType("INFORMATIONPOST"); + + + Post expectedPost = createPost("제목", "내용", member, Post.PostType.INFORMATIONPOST); + + when(postRepository.save(any(Post.class))).thenReturn(expectedPost); + + // when + Post result = postService.createPost(request, member); + + // then + assertThat(result.getTitle()).isEqualTo("제목"); + assertThat(result.getContent()).isEqualTo("내용"); + assertThat(result.getMember()).isEqualTo(member); + assertThat(result.getPostType()).isEqualTo(Post.PostType.INFORMATIONPOST); + verify(postRepository).save(any(Post.class)); + } + + @Test + @DisplayName("멘토가 실무 경험 공유 게시글 생성 성공") + void createPost_practicePost_mentor_success() { + // given + Member mentor = MemberFixture.create(1L, "mentor@test.com", "Mentor", "password", Member.Role.MENTOR); + PostCreateRequest request = new PostCreateRequest(); + request.setContent("실무내용"); + request.setTitle("실무경험"); + request.setPostType("PRACTICEPOST"); + Post expectedPost = createPost("실무 경험", "실무 내용", mentor, Post.PostType.PRACTICEPOST); + + when(postRepository.save(any(Post.class))).thenReturn(expectedPost); + + // when + Post result = postService.createPost(request, mentor); + + // then + assertThat(result.getPostType()).isEqualTo(Post.PostType.PRACTICEPOST); + verify(postRepository).save(any(Post.class)); + } + + @Test + @DisplayName("멘티가 실무 경험 공유 게시글 생성 실패") + void createPost_practicePost_mentee_failure() { + // given + Member mentee = MemberFixture.create(1L, "mentee@test.com", "Mentee", "password", Member.Role.MENTEE); + PostCreateRequest request = new PostCreateRequest(); + request.setContent("실무내용"); + request.setTitle("실무경험"); + request.setPostType("PRACTICEPOST"); + + // when & then + assertThatThrownBy(() -> postService.createPost(request, mentee)) + .isInstanceOf(ServiceException.class) + .hasMessage("400 : 실무 경험 공유 게시글은 멘토만 작성할 수 있습니다."); + + verify(postRepository, never()).save(any(Post.class)); + } + + @Test + @DisplayName("질문 게시글 생성 시 isResolve false로 초기화") + void createPost_questionPost_initializeIsResolve() { + // given + Member member = MemberFixture.create(1L, "test@test.com", "Test User", "password", Member.Role.MENTEE); + PostCreateRequest request = new PostCreateRequest(); + request.setContent("질문내용"); + request.setTitle("질문경험"); + request.setPostType("QUESTIONPOST"); + Post expectedPost = createPost("질문", "질문 내용", member, Post.PostType.QUESTIONPOST); + expectedPost.updateResolveStatus(false); + + when(postRepository.save(any(Post.class))).thenReturn(expectedPost); + + // when + Post result = postService.createPost(request, member); + + // then + assertThat(result.getPostType()).isEqualTo(Post.PostType.QUESTIONPOST); + assertThat(result.getIsResolve()).isFalse(); + verify(postRepository).save(any(Post.class)); + } + + @Test + @DisplayName("유효하지 않은 PostType으로 게시글 생성 실패") + void createPost_invalidPostType_failure() { + // given + Member member = MemberFixture.createDefault(); + PostCreateRequest request = new PostCreateRequest(); + request.setContent("실무내용"); + request.setTitle("실무경험"); + request.setPostType("INVALIDPOST"); + + // when & then + assertThatThrownBy(() -> postService.createPost(request, member)) + .isInstanceOf(ServiceException.class) + .hasMessage("400 : 유효하지 않은 PostType입니다."); + + verify(postRepository, never()).save(any(Post.class)); + } + } + + @Nested + @DisplayName("게시글 삭제 테스트") + class RemovePostTest { + + @Test + @DisplayName("작성자가 게시글 삭제 성공") + void removePost_author_success() { + // given + Member author = MemberFixture.create(1L, "author@test.com", "Author", "password", Member.Role.MENTEE); + Post post = createPost("제목", "내용", author, Post.PostType.INFORMATIONPOST); + Long postId = 1L; + + when(postRepository.findById(postId)).thenReturn(Optional.of(post)); + + // when + postService.removePost(postId, author); + + // then + verify(postRepository).delete(post); + } + + @Test + @DisplayName("작성자가 아닌 사용자가 게시글 삭제 시도 시 실패") + void removePost_notAuthor_failure() { + // given + Member author = MemberFixture.create(1L, "author@test.com", "Author", "password", Member.Role.MENTEE); + Member otherUser = MemberFixture.create(2L, "other@test.com", "Other", "password", Member.Role.MENTEE); + Post post = createPost("제목", "내용", author, Post.PostType.INFORMATIONPOST); + Long postId = 1L; + + when(postRepository.findById(postId)).thenReturn(Optional.of(post)); + + // when & then + assertThatThrownBy(() -> postService.removePost(postId, otherUser)) + .isInstanceOf(ServiceException.class) + .hasMessage("400 : 삭제 권한이 없습니다."); + + verify(postRepository, never()).delete(any(Post.class)); + } + + @Test + @DisplayName("존재하지 않는 게시글 삭제 시도 시 실패") + void removePost_notExists_failure() { + // given + Member member = MemberFixture.createDefault(); + Long postId = 999L; + + when(postRepository.findById(postId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> postService.removePost(postId, member)) + .isInstanceOf(ServiceException.class) + .hasMessage("400 : 해당 Id의 게시글이 없습니다."); + + verify(postRepository, never()).delete(any(Post.class)); + } + } + + @Nested + @DisplayName("게시글 수정 테스트") + class UpdatePostTest { + + @Test + @DisplayName("작성자가 게시글 수정 성공") + void updatePost_author_success() { + // given + Member author = MemberFixture.create(1L, "author@test.com", "Author", "password", Member.Role.MENTEE); + Post post = createPost("기존 제목", "기존 내용", author, Post.PostType.INFORMATIONPOST); + Long postId = 1L; + PostCreateRequest updateRequest = new PostCreateRequest(); + updateRequest.setTitle("새 제목"); + updateRequest.setContent("새 내용"); + updateRequest.setPostType("INFORMATIONPOST"); + + when(postRepository.findById(postId)).thenReturn(Optional.of(post)); + when(postRepository.save(any(Post.class))).thenReturn(post); + + // when + postService.updatePost(postId, author, updateRequest); + + // then + verify(postRepository).save(post); + assertThat(post.getTitle()).isEqualTo("새 제목"); + assertThat(post.getContent()).isEqualTo("새 내용"); + } + + @Test + @DisplayName("작성자가 아닌 사용자가 게시글 수정 시도 시 실패") + void updatePost_notAuthor_failure() { + // given + Member author = MemberFixture.create(1L, "author@test.com", "Author", "password", Member.Role.MENTEE); + Member otherUser = MemberFixture.create(2L, "other@test.com", "Other", "password", Member.Role.MENTEE); + Post post = createPost("제목", "내용", author, Post.PostType.INFORMATIONPOST); + Long postId = 1L; + PostCreateRequest updateRequest = new PostCreateRequest(); + updateRequest.setTitle("새 제목"); + updateRequest.setContent("새 내용"); + updateRequest.setPostType("INFORMATIONPOST"); + + when(postRepository.findById(postId)).thenReturn(Optional.of(post)); + + // when & then + assertThatThrownBy(() -> postService.updatePost(postId, otherUser, updateRequest)) + .isInstanceOf(ServiceException.class) + .hasMessage("400 : 수정 권한이 없습니다."); + + verify(postRepository, never()).save(any(Post.class)); + } + + @Test + @DisplayName("제목이 null이거나 공백일 때 수정 실패") + void updatePost_nullOrBlankTitle_failure() { + // given + Member author = MemberFixture.create(1L, "author@test.com", "Author", "password", Member.Role.MENTEE); + Post post = createPost("제목", "내용", author, Post.PostType.INFORMATIONPOST); + Long postId = 1L; + PostCreateRequest updateRequest = new PostCreateRequest(); + updateRequest.setTitle(""); + updateRequest.setContent("새 내용"); + updateRequest.setPostType("INFORMATIONPOST"); + + when(postRepository.findById(postId)).thenReturn(Optional.of(post)); + + // when & then + assertThatThrownBy(() -> postService.updatePost(postId, author, updateRequest)) + .isInstanceOf(ServiceException.class) + .hasMessage("400 : 제목을 입력해주세요."); + + verify(postRepository, never()).save(any(Post.class)); + } + + @Test + @DisplayName("내용이 null이거나 공백일 때 수정 실패") + void updatePost_nullOrBlankContent_failure() { + // given + Member author = MemberFixture.create(1L, "author@test.com", "Author", "password", Member.Role.MENTEE); + Post post = createPost("제목", "내용", author, Post.PostType.INFORMATIONPOST); + Long postId = 1L; + PostCreateRequest updateRequest = new PostCreateRequest(); + updateRequest.setTitle("새 제목"); + updateRequest.setContent(""); + updateRequest.setPostType("INFORMATIONPOST"); + + when(postRepository.findById(postId)).thenReturn(Optional.of(post)); + + // when & then + assertThatThrownBy(() -> postService.updatePost(postId, author, updateRequest)) + .isInstanceOf(ServiceException.class) + .hasMessage("400 : 내용을 입력해주세요."); + + verify(postRepository, never()).save(any(Post.class)); + } + } + + @Nested + @DisplayName("게시글 조회 테스트") + class GetPostTest { + + @Test + @DisplayName("모든 게시글 조회 성공") + void getAllPosts_success() { + // given + Member member1 = MemberFixture.create(1L, "user1@test.com", "User1", "password", Member.Role.MENTEE); + Member member2 = MemberFixture.create(2L, "user2@test.com", "User2", "password", Member.Role.MENTOR); + + List posts = Arrays.asList( + createPost("제목1", "내용1", member1, Post.PostType.INFORMATIONPOST), + createPost("제목2", "내용2", member2, Post.PostType.PRACTICEPOST) + ); + + when(postRepository.findAll()).thenReturn(posts); + + // when + List result = postService.getAllPosts(); + + // then + assertThat(result).hasSize(2); + assertThat(result).containsExactlyElementsOf(posts); + verify(postRepository).findAll(); + } + + @Test + @DisplayName("게시글 상세 조회 시 조회수 증가") + void getPostDetailWithViewIncrement_success() { + // given + Member member = MemberFixture.create(1L, "user@test.com", "User", "password", Member.Role.MENTEE); + Post post = createPost("제목", "내용", member, Post.PostType.INFORMATIONPOST); + Long postId = 1L; + int initialViewCount = post.getViewCount(); + + when(postRepository.findByIdWithMember(postId)).thenReturn(Optional.of(post)); + + // when + Post result = postService.getPostDetailWithViewIncrement(postId); + + // then + assertThat(result.getViewCount()).isEqualTo(initialViewCount + 1); + verify(postRepository).findByIdWithMember(postId); + } + + @Test + @DisplayName("존재하지 않는 게시글 상세 조회 시 실패") + void getPostDetailWithViewIncrement_notExists_failure() { + // given + Long postId = 999L; + + when(postRepository.findByIdWithMember(postId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> postService.getPostDetailWithViewIncrement(postId)) + .isInstanceOf(ServiceException.class) + .hasMessage("400 : 해당 Id의 게시글이 없습니다."); + } + + @Test + @DisplayName("페이징으로 게시글 검색 성공") + void getPosts_withPaging_success() { + // given + String keyword = "테스트"; + int page = 0; + int size = 10; + Post.PostType postType = Post.PostType.INFORMATIONPOST; + Pageable pageable = PageRequest.of(page, size); + + Member member = MemberFixture.create(1L, "user@test.com", "User", "password", Member.Role.MENTEE); + Post post = createPost("테스트 제목", "테스트 내용", member, Post.PostType.INFORMATIONPOST); + List posts = Arrays.asList(post); + Page postPage = new PageImpl<>(posts, pageable, 1); + + when(postRepository.searchPosts(keyword, pageable, postType)).thenReturn(postPage); + + // when + Page result = postService.getPosts(keyword, page, size, postType); + + // then + assertThat(result.getContent()).hasSize(1); + assertThat(result.getTotalElements()).isEqualTo(1); + verify(postRepository).searchPosts(keyword, pageable, postType); + } + + @Test + @DisplayName("ID로 게시글 찾기 성공") + void findById_success() { + // given + Member member = MemberFixture.create(1L, "user@test.com", "User", "password", Member.Role.MENTEE); + Post post = createPost("제목", "내용", member, Post.PostType.INFORMATIONPOST); + Long postId = 1L; + + when(postRepository.findById(postId)).thenReturn(Optional.of(post)); + + // when + Post result = postService.findById(postId); + + // then + assertThat(result).isEqualTo(post); + verify(postRepository).findById(postId); + } + + @Test + @DisplayName("존재하지 않는 ID로 게시글 찾기 실패") + void findById_notExists_failure() { + // given + Long postId = 999L; + + when(postRepository.findById(postId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> postService.findById(postId)) + .isInstanceOf(ServiceException.class) + .hasMessage("400 : 해당 Id의 게시글이 없습니다."); + } + } + + private Post createPost(String title, String content, Member member, Post.PostType postType) { + return Post.builder() + .title(title) + .content(content) + .member(member) + .postType(postType) + .build(); + } +}