diff --git a/src/test/java/com/potatocake/everymoment/controller/CommentControllerTest.java b/src/test/java/com/potatocake/everymoment/controller/CommentControllerTest.java new file mode 100644 index 0000000..7130077 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/controller/CommentControllerTest.java @@ -0,0 +1,139 @@ +package com.potatocake.everymoment.controller; + +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.willDoNothing; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.potatocake.everymoment.dto.request.CommentRequest; +import com.potatocake.everymoment.entity.Member; +import com.potatocake.everymoment.security.MemberDetails; +import com.potatocake.everymoment.service.CommentService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +@WebMvcTest(CommentController.class) +class CommentControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private CommentService commentService; + + @Test + @DisplayName("댓글이 성공적으로 수정된다.") + void should_UpdateComment_When_ValidInput() throws Exception { + // given + Long memberId = 1L; + Long commentId = 1L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + CommentRequest request = new CommentRequest(); + request.setContent("Updated comment"); + + willDoNothing().given(commentService).updateComment( + eq(memberId), + eq(commentId), + argThat(req -> req.getContent().equals("Updated comment")) + ); + + // when + ResultActions result = mockMvc.perform(patch("/api/comments/{commentId}", commentId) + .with(user(memberDetails)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")); + + then(commentService).should().updateComment( + eq(memberId), + eq(commentId), + argThat(req -> req.getContent().equals("Updated comment")) + ); + } + + @Test + @DisplayName("댓글이 성공적으로 삭제된다.") + void should_DeleteComment_When_ValidId() throws Exception { + // given + Long memberId = 1L; + Long commentId = 1L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + willDoNothing().given(commentService).deleteComment(memberId, commentId); + + // when + ResultActions result = mockMvc.perform(delete("/api/comments/{commentId}", commentId) + .with(user(memberDetails)) + .with(csrf())); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")); + + then(commentService).should().deleteComment(memberId, commentId); + } + + @Test + @DisplayName("댓글 내용이 누락되면 수정에 실패한다.") + void should_FailToUpdate_When_ContentMissing() throws Exception { + // given + Long memberId = 1L; + Long commentId = 1L; + Member member = Member.builder() + .id(memberId) + .number(1234L) + .nickname("testUser") + .build(); + MemberDetails memberDetails = new MemberDetails(member); + + CommentRequest request = new CommentRequest(); + // content 누락 + + // when + ResultActions result = mockMvc.perform(patch("/api/comments/{commentId}", commentId) + .with(user(memberDetails)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + // then + result.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(400)); + + then(commentService).shouldHaveNoInteractions(); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/entity/CommentTest.java b/src/test/java/com/potatocake/everymoment/entity/CommentTest.java new file mode 100644 index 0000000..1a9b07c --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/entity/CommentTest.java @@ -0,0 +1,41 @@ +package com.potatocake.everymoment.entity; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CommentTest { + + @Test + @DisplayName("댓글 내용이 성공적으로 업데이트된다.") + void should_UpdateContent_When_NewContentProvided() { + // given + Comment comment = Comment.builder() + .content("Original content") + .build(); + String newContent = "Updated content"; + + // when + comment.updateContent(newContent); + + // then + assertThat(comment.getContent()).isEqualTo(newContent); + } + + @Test + @DisplayName("null 내용으로 업데이트하면 기존 내용이 유지된다.") + void should_KeepOriginalContent_When_NullContentProvided() { + // given + Comment comment = Comment.builder() + .content("Original content") + .build(); + + // when + comment.updateContent(null); + + // then + assertThat(comment.getContent()).isEqualTo("Original content"); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/repository/CommentRepositoryTest.java b/src/test/java/com/potatocake/everymoment/repository/CommentRepositoryTest.java new file mode 100644 index 0000000..c2ec107 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/repository/CommentRepositoryTest.java @@ -0,0 +1,240 @@ +package com.potatocake.everymoment.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.potatocake.everymoment.entity.Comment; +import com.potatocake.everymoment.entity.Diary; +import com.potatocake.everymoment.entity.Member; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.test.context.TestPropertySource; + +@TestPropertySource(properties = "spring.jpa.hibernate.ddl-auto=create-drop") +@DataJpaTest +class CommentRepositoryTest { + + @Autowired + private CommentRepository commentRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private DiaryRepository diaryRepository; + + @Test + @DisplayName("댓글이 성공적으로 저장된다.") + void should_SaveComment_When_ValidEntity() { + // given + Member member = createAndSaveMember(); + Diary diary = createAndSaveDiary(member); + + Comment comment = Comment.builder() + .member(member) + .diary(diary) + .content("Test comment") + .build(); + + // when + Comment savedComment = commentRepository.save(comment); + + // then + assertThat(savedComment.getId()).isNotNull(); + assertThat(savedComment.getContent()).isEqualTo("Test comment"); + assertThat(savedComment.getMember()).isEqualTo(member); + assertThat(savedComment.getDiary()).isEqualTo(diary); + } + + @Test + @DisplayName("일기의 댓글 목록이 성공적으로 조회된다.") + void should_FindComments_When_FilteringByDiaryId() { + // given + Member member = createAndSaveMember(); + Diary diary = createAndSaveDiary(member); + + Comment comment1 = Comment.builder() + .member(member) + .diary(diary) + .content("Comment 1") + .build(); + + Comment comment2 = Comment.builder() + .member(member) + .diary(diary) + .content("Comment 2") + .build(); + + commentRepository.saveAll(List.of(comment1, comment2)); + + // when + Page comments = commentRepository.findAllByDiaryId( + diary.getId(), + PageRequest.of(0, 10) + ); + + // then + assertThat(comments.getContent()).hasSize(2); + assertThat(comments.getContent()) + .extracting("content") + .containsExactly("Comment 1", "Comment 2"); + } + + @Test + @DisplayName("페이징이 성공적으로 동작한다.") + void should_ReturnPagedResult_When_UsingPagination() { + // given + Member member = createAndSaveMember(); + Diary diary = createAndSaveDiary(member); + + List comments = List.of( + Comment.builder() + .member(member) + .diary(diary) + .content("Comment 1") + .build(), + Comment.builder() + .member(member) + .diary(diary) + .content("Comment 2") + .build(), + Comment.builder() + .member(member) + .diary(diary) + .content("Comment 3") + .build() + ); + + commentRepository.saveAll(comments); + + // when + Page firstPage = commentRepository.findAllByDiaryId( + diary.getId(), + PageRequest.of(0, 2) + ); + + Page secondPage = commentRepository.findAllByDiaryId( + diary.getId(), + PageRequest.of(1, 2) + ); + + // then + assertThat(firstPage.getContent()).hasSize(2); + assertThat(firstPage.hasNext()).isTrue(); + assertThat(firstPage.getContent()) + .extracting("content") + .containsExactly("Comment 1", "Comment 2"); + + assertThat(secondPage.getContent()).hasSize(1); + assertThat(secondPage.hasNext()).isFalse(); + assertThat(secondPage.getContent()) + .extracting("content") + .containsExactly("Comment 3"); + } + + @Test + @DisplayName("다른 일기의 댓글은 조회되지 않는다.") + void should_NotFindComments_When_DifferentDiary() { + // given + Member member = createAndSaveMember(); + Diary diary1 = createAndSaveDiary(member); + Diary diary2 = createAndSaveDiary(member); + + Comment comment1 = Comment.builder() + .member(member) + .diary(diary1) + .content("Comment for diary 1") + .build(); + + Comment comment2 = Comment.builder() + .member(member) + .diary(diary2) + .content("Comment for diary 2") + .build(); + + commentRepository.saveAll(List.of(comment1, comment2)); + + // when + Page comments = commentRepository.findAllByDiaryId( + diary1.getId(), + PageRequest.of(0, 10) + ); + + // then + assertThat(comments.getContent()).hasSize(1); + assertThat(comments.getContent()) + .extracting("content") + .containsExactly("Comment for diary 1"); + } + + @Test + @DisplayName("빈 페이지가 성공적으로 반환된다.") + void should_ReturnEmptyPage_When_NoComments() { + // given + Member member = createAndSaveMember(); + Diary diary = createAndSaveDiary(member); + + // when + Page comments = commentRepository.findAllByDiaryId( + diary.getId(), + PageRequest.of(0, 10) + ); + + // then + assertThat(comments.getContent()).isEmpty(); + assertThat(comments.getTotalElements()).isZero(); + assertThat(comments.hasNext()).isFalse(); + } + + @Test + @DisplayName("댓글이 성공적으로 삭제된다.") + void should_DeleteComment_When_ValidEntity() { + // given + Member member = createAndSaveMember(); + Diary diary = createAndSaveDiary(member); + Comment comment = Comment.builder() + .member(member) + .diary(diary) + .content("Test comment") + .build(); + Comment savedComment = commentRepository.save(comment); + + // when + commentRepository.delete(savedComment); + + // then + Optional foundComment = commentRepository.findById(savedComment.getId()); + assertThat(foundComment).isEmpty(); + } + + private Member createAndSaveMember() { + Member member = Member.builder() + .number(1234L) + .nickname("testUser") + .profileImageUrl("https://example.com/image.jpg") + .build(); + return memberRepository.save(member); + } + + private Diary createAndSaveDiary(Member member) { + Point point = new GeometryFactory().createPoint(new Coordinate(37.5665, 126.978)); + + Diary diary = Diary.builder() + .member(member) + .content("Test diary") + .locationName("Test location") + .address("Test address") + .locationPoint(point) + .build(); + return diaryRepository.save(diary); + } + +} diff --git a/src/test/java/com/potatocake/everymoment/service/CommentServiceTest.java b/src/test/java/com/potatocake/everymoment/service/CommentServiceTest.java new file mode 100644 index 0000000..da7f913 --- /dev/null +++ b/src/test/java/com/potatocake/everymoment/service/CommentServiceTest.java @@ -0,0 +1,219 @@ +package com.potatocake.everymoment.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; + +import com.potatocake.everymoment.constant.NotificationType; +import com.potatocake.everymoment.dto.request.CommentRequest; +import com.potatocake.everymoment.dto.response.CommentsResponse; +import com.potatocake.everymoment.entity.Comment; +import com.potatocake.everymoment.entity.Diary; +import com.potatocake.everymoment.entity.Member; +import com.potatocake.everymoment.exception.ErrorCode; +import com.potatocake.everymoment.exception.GlobalException; +import com.potatocake.everymoment.repository.CommentRepository; +import com.potatocake.everymoment.repository.DiaryRepository; +import com.potatocake.everymoment.repository.MemberRepository; +import java.util.List; +import java.util.Optional; +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 org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; + +@ExtendWith(MockitoExtension.class) +class CommentServiceTest { + + @InjectMocks + private CommentService commentService; + + @Mock + private CommentRepository commentRepository; + + @Mock + private DiaryRepository diaryRepository; + + @Mock + private MemberRepository memberRepository; + + @Mock + private NotificationService notificationService; + + @Test + @DisplayName("댓글 목록이 성공적으로 조회된다.") + void should_GetComments_When_ValidDiaryId() { + // given + Long diaryId = 1L; + Member member = Member.builder() + .id(1L) + .nickname("testUser") + .build(); + Comment comment = Comment.builder() + .id(1L) + .member(member) + .content("Test comment") + .build(); + + Page commentPage = new PageImpl<>(List.of(comment)); + + given(commentRepository.findAllByDiaryId(eq(diaryId), any(PageRequest.class))) + .willReturn(commentPage); + + // when + CommentsResponse response = commentService.getComments(diaryId, 0, 10); + + // then + assertThat(response.getComments()).hasSize(1); + assertThat(response.getComments().get(0).getContent()).isEqualTo("Test comment"); + then(commentRepository).should().findAllByDiaryId(eq(diaryId), any(PageRequest.class)); + } + + @Test + @DisplayName("댓글이 성공적으로 생성된다.") + void should_CreateComment_When_ValidInput() { + // given + Long memberId = 1L; + Long diaryId = 1L; + Member member = Member.builder() + .id(memberId) + .nickname("testUser") + .build(); + + Member diaryOwner = Member.builder() + .id(2L) + .build(); + + Diary diary = Diary.builder() + .id(diaryId) + .member(diaryOwner) + .isPublic(true) + .build(); + + CommentRequest request = new CommentRequest(); + request.setContent("New comment"); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(diaryRepository.findById(diaryId)).willReturn(Optional.of(diary)); + given(commentRepository.save(any(Comment.class))).willReturn( + Comment.builder() + .id(1L) + .member(member) + .diary(diary) + .content(request.getContent()) + .build() + ); + + // when + commentService.createComment(memberId, diaryId, request); + + // then + then(commentRepository).should().save(any(Comment.class)); + then(notificationService).should().createAndSendNotification( + eq(diary.getMember().getId()), + eq(NotificationType.COMMENT), + eq(diaryId), + eq(member.getNickname()) + ); + } + + @Test + @DisplayName("자신의 일기에 댓글을 작성할 때는 알림이 발송되지 않는다.") + void should_NotSendNotification_When_CommentingOwnDiary() { + // given + Long memberId = 1L; + Long diaryId = 1L; + Member member = Member.builder() + .id(memberId) + .nickname("testUser") + .build(); + + Diary diary = Diary.builder() + .id(diaryId) + .member(member) // 일기 작성자와 댓글 작성자가 동일 + .isPublic(true) + .build(); + + CommentRequest request = new CommentRequest(); + request.setContent("New comment"); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(diaryRepository.findById(diaryId)).willReturn(Optional.of(diary)); + given(commentRepository.save(any(Comment.class))).willReturn( + Comment.builder() + .id(1L) + .member(member) + .diary(diary) + .content(request.getContent()) + .build() + ); + + // when + commentService.createComment(memberId, diaryId, request); + + // then + then(commentRepository).should().save(any(Comment.class)); + then(notificationService).shouldHaveNoInteractions(); + } + + @Test + @DisplayName("비공개 일기에 댓글을 작성하려고 하면 예외가 발생한다.") + void should_ThrowException_When_DiaryNotPublic() { + // given + Long memberId = 1L; + Long diaryId = 1L; + Member member = Member.builder() + .id(memberId) + .build(); + Diary diary = Diary.builder() + .id(diaryId) + .isPublic(false) + .build(); + CommentRequest request = new CommentRequest(); + request.setContent("New comment"); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(diaryRepository.findById(diaryId)).willReturn(Optional.of(diary)); + + // when & then + assertThatThrownBy(() -> commentService.createComment(memberId, diaryId, request)) + .isInstanceOf(GlobalException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.DIARY_NOT_PUBLIC); + } + + @Test + @DisplayName("댓글이 성공적으로 수정된다.") + void should_UpdateComment_When_ValidInput() { + // given + Long memberId = 1L; + Long commentId = 1L; + Member member = Member.builder() + .id(memberId) + .build(); + Comment comment = Comment.builder() + .id(commentId) + .member(member) + .content("Original content") + .build(); + CommentRequest request = new CommentRequest(); + request.setContent("Updated content"); + + given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); + given(commentRepository.findById(commentId)).willReturn(Optional.of(comment)); + + // when + commentService.updateComment(memberId, commentId, request); + + // then + assertThat(comment.getContent()).isEqualTo("Updated content"); + } + +}