From e88bad95df66b43b4cde103c099a8720afd1e042 Mon Sep 17 00:00:00 2001 From: JiwonDev Date: Fri, 22 Oct 2021 02:47:14 +0900 Subject: [PATCH] =?UTF-8?q?Add=20comment=20query=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EA=B8=80=20=EB=8C=93=EA=B8=80=20=EC=A1=B0=ED=9A=8C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/application/CommentFacade.java | 50 +++++++++++++++- .../comment/presentation/CommentQueryApi.java | 32 ++++++++++ .../comment/presentation/CommentResponse.java | 19 ++++++ .../member/presentation/MemberResponse.java | 13 +++++ .../presentation/ArticleQueryApiTest.java | 2 +- .../comment/presentation/CommentApiUri.java | 1 + .../presentation/CommentQueryApiTest.java | 48 +++++++++++++++ .../integration/ArticleIntegrationTests.java | 58 +++++++++++++++++-- .../integration/CommentIntegrationTests.java | 48 +++++++++++++++ 9 files changed, 265 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/yam/app/comment/presentation/CommentQueryApi.java create mode 100644 src/main/java/com/yam/app/comment/presentation/CommentResponse.java create mode 100644 src/main/java/com/yam/app/member/presentation/MemberResponse.java create mode 100644 src/test/java/com/yam/app/comment/presentation/CommentQueryApiTest.java diff --git a/src/main/java/com/yam/app/comment/application/CommentFacade.java b/src/main/java/com/yam/app/comment/application/CommentFacade.java index 4a383fd..8c6fc70 100644 --- a/src/main/java/com/yam/app/comment/application/CommentFacade.java +++ b/src/main/java/com/yam/app/comment/application/CommentFacade.java @@ -1,8 +1,17 @@ package com.yam.app.comment.application; +import com.yam.app.article.domain.ArticleNotFoundException; +import com.yam.app.article.domain.ArticleReader; +import com.yam.app.comment.domain.Comment; import com.yam.app.comment.domain.CommentProcessor; +import com.yam.app.comment.domain.CommentReader; +import com.yam.app.comment.presentation.CommentResponse; import com.yam.app.comment.presentation.CreateCommentCommand; import com.yam.app.comment.presentation.UpdateCommentCommand; +import com.yam.app.member.domain.MemberReader; +import com.yam.app.member.presentation.MemberResponse; +import java.util.List; +import java.util.stream.Collectors; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -10,9 +19,17 @@ import org.springframework.transaction.annotation.Transactional; public class CommentFacade { private final CommentProcessor commentProcessor; + private final ArticleReader articleReader; + private final CommentReader commentReader; + private final MemberReader memberReader; - public CommentFacade(CommentProcessor commentProcessor) { + public CommentFacade(CommentProcessor commentProcessor, + ArticleReader articleReader, CommentReader commentReader, + MemberReader memberReader) { this.commentProcessor = commentProcessor; + this.articleReader = articleReader; + this.commentReader = commentReader; + this.memberReader = memberReader; } @Transactional @@ -29,4 +46,35 @@ public class CommentFacade { public void delete(Long commentId, Long memberId) { commentProcessor.delete(commentId, memberId); } + + @Transactional(readOnly = true) + public List findByArticleId(Long articleId) { + if (!articleReader.existsById(articleId)) { + throw new ArticleNotFoundException(articleId); + } + + return commentReader.findByArticleId(articleId) + .stream() + .filter(Comment::isAlive) + .map(comment -> { + var builder = CommentResponse.builder(); + builder.id(comment.getId()); + builder.articleId(comment.getArticleId()); + builder.content(comment.getContent()); + builder.createAt(comment.getCreatedAt()); + builder.modifiedAt(comment.getModifiedAt()); + + var member = memberReader.findById(comment.getMemberId()) + .orElseThrow(IllegalStateException::new); + + builder.member(MemberResponse.builder() + .id(member.getId()) + .image(member.getImage()) + .nickname(member.getNickname()) + .build()); + + return builder.build(); + }) + .collect(Collectors.toList()); + } } diff --git a/src/main/java/com/yam/app/comment/presentation/CommentQueryApi.java b/src/main/java/com/yam/app/comment/presentation/CommentQueryApi.java new file mode 100644 index 0000000..2a708b3 --- /dev/null +++ b/src/main/java/com/yam/app/comment/presentation/CommentQueryApi.java @@ -0,0 +1,32 @@ +package com.yam.app.comment.presentation; + +import com.yam.app.comment.application.CommentFacade; +import com.yam.app.common.ApiResult; +import java.util.List; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping( + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE +) +public final class CommentQueryApi { + + private final CommentFacade commentFacade; + + public CommentQueryApi(CommentFacade commentFacade) { + this.commentFacade = commentFacade; + } + + @GetMapping("/api/comments/{articleId}") + public ResponseEntity>> getComments( + @PathVariable Long articleId) { + return ResponseEntity.ok( + ApiResult.success(commentFacade.findByArticleId(articleId))); + } +} diff --git a/src/main/java/com/yam/app/comment/presentation/CommentResponse.java b/src/main/java/com/yam/app/comment/presentation/CommentResponse.java new file mode 100644 index 0000000..56cbb66 --- /dev/null +++ b/src/main/java/com/yam/app/comment/presentation/CommentResponse.java @@ -0,0 +1,19 @@ +package com.yam.app.comment.presentation; + +import com.yam.app.member.presentation.MemberResponse; +import java.time.LocalDateTime; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public final class CommentResponse { + + private final Long id; + private final String content; + private final LocalDateTime createAt; + private final LocalDateTime modifiedAt; + private final Long articleId; + private final MemberResponse member; + +} diff --git a/src/main/java/com/yam/app/member/presentation/MemberResponse.java b/src/main/java/com/yam/app/member/presentation/MemberResponse.java new file mode 100644 index 0000000..be7a272 --- /dev/null +++ b/src/main/java/com/yam/app/member/presentation/MemberResponse.java @@ -0,0 +1,13 @@ +package com.yam.app.member.presentation; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public final class MemberResponse { + + private final Long id; + private final String nickname; + private final String image; +} diff --git a/src/test/java/com/yam/app/article/presentation/ArticleQueryApiTest.java b/src/test/java/com/yam/app/article/presentation/ArticleQueryApiTest.java index 3d5a711..ea0a1b4 100644 --- a/src/test/java/com/yam/app/article/presentation/ArticleQueryApiTest.java +++ b/src/test/java/com/yam/app/article/presentation/ArticleQueryApiTest.java @@ -15,7 +15,7 @@ import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; -@DisplayName("Article Qurey HTTP API") +@DisplayName("Article Query HTTP API") @WebMvcTest(ArticleQueryApi.class) @ActiveProfiles("test") class ArticleQueryApiTest { diff --git a/src/test/java/com/yam/app/comment/presentation/CommentApiUri.java b/src/test/java/com/yam/app/comment/presentation/CommentApiUri.java index c73ce61..09143f7 100644 --- a/src/test/java/com/yam/app/comment/presentation/CommentApiUri.java +++ b/src/test/java/com/yam/app/comment/presentation/CommentApiUri.java @@ -5,6 +5,7 @@ public final class CommentApiUri { public static final String CREATE_COMMENT = "/api/comments/"; public static final String UPDATE_COMMENT = "/api/comments/"; public static final String DELETE_COMMENT = "/api/comments/"; + public static final String FIND_BY_ARTICLE_ID = "/api/comments/"; private CommentApiUri() { } diff --git a/src/test/java/com/yam/app/comment/presentation/CommentQueryApiTest.java b/src/test/java/com/yam/app/comment/presentation/CommentQueryApiTest.java new file mode 100644 index 0000000..bf1855e --- /dev/null +++ b/src/test/java/com/yam/app/comment/presentation/CommentQueryApiTest.java @@ -0,0 +1,48 @@ +package com.yam.app.comment.presentation; + +import static com.yam.app.comment.presentation.CommentApiUri.FIND_BY_ARTICLE_ID; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +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.yam.app.comment.application.CommentFacade; +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.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +@DisplayName("Comment Query HTTP API") +@WebMvcTest(CommentQueryApi.class) +@ActiveProfiles("test") +class CommentQueryApiTest { + + @Autowired + private MockMvc mockMvc; + @Autowired + private ObjectMapper objectMapper; + @MockBean + private CommentFacade commentFacade; + + @Test + @DisplayName("인증되지 않은 사용자가 댓글 조회 요청을 보냈다면 401에러를 반환한다.") + void unauthenticated_user_request() throws Exception { + //Act + final var actions = mockMvc.perform(get(FIND_BY_ARTICLE_ID + 1) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)); + + //Assert + actions + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.data").doesNotExist()) + .andExpect(jsonPath("$.message").value("Unauthorized request")); + + } + +} diff --git a/src/test/java/com/yam/app/integration/ArticleIntegrationTests.java b/src/test/java/com/yam/app/integration/ArticleIntegrationTests.java index 513057e..bda4092 100644 --- a/src/test/java/com/yam/app/integration/ArticleIntegrationTests.java +++ b/src/test/java/com/yam/app/integration/ArticleIntegrationTests.java @@ -2,10 +2,10 @@ package com.yam.app.integration; import static com.yam.app.account.presentation.AccountApiUri.LOGIN; import static com.yam.app.article.presentation.ArticleApiUri.FIND_ALL; +import static com.yam.app.article.presentation.ArticleApiUri.FIND_BY_ID; import static com.yam.app.article.presentation.ArticleApiUri.WRITE_ARTICLE; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -64,10 +64,60 @@ final class ArticleIntegrationTests extends AbstractIntegrationTests { void default_main_page_find_all_preview_article_response() throws Exception { // Act mockMvc.perform(get(FIND_ALL) - .accept(MediaType.APPLICATION_JSON) - .contentType(MediaType.APPLICATION_JSON) - ) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + ) .andExpect(status().isOk()) .andExpect(jsonPath("$.data").isArray()); } + + @ParameterizedTest + @AutoSource + @DisplayName("로그인에 적절한 파라미터를 입력하여, 성공하고 " + + "인증된 사용자가 존재하는 게시글, 존재하지 않는 게시글을 각각 조회하는 시나리오 테스트.") + void login_success_and_get_article_by_id() throws Exception { + //Arrange + var loginCommand = new LoginAccountCommand(); + loginCommand.setEmail("loginCheck@gmail.com"); + loginCommand.setPassword("password!"); + + var articleId = 1L; + var invalidArticleId = 9999L; + + // Act & Assert + mockMvc.perform(post(LOGIN) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(loginCommand)) + ) + .andExpect(status().isOk()) + .andDo( + result -> { + final var actions = mockMvc.perform(get(FIND_BY_ID + articleId) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + ); + + actions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").isNumber()) + .andExpect(jsonPath("$.authorId").isNumber()) + .andExpect(jsonPath("$.title").isString()) + .andExpect(jsonPath("$.content").isString()) + .andExpect(jsonPath("$.image").isString()) + .andExpect(jsonPath("$.tags").isArray()); + }) + .andDo( + result -> { + final var actions = mockMvc.perform(get(FIND_BY_ID + invalidArticleId) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + ); + + actions + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.data").doesNotExist()); + }); + } } diff --git a/src/test/java/com/yam/app/integration/CommentIntegrationTests.java b/src/test/java/com/yam/app/integration/CommentIntegrationTests.java index d4c563f..0c1fdcb 100644 --- a/src/test/java/com/yam/app/integration/CommentIntegrationTests.java +++ b/src/test/java/com/yam/app/integration/CommentIntegrationTests.java @@ -3,11 +3,14 @@ package com.yam.app.integration; import static com.yam.app.account.presentation.AccountApiUri.LOGIN; import static com.yam.app.comment.presentation.CommentApiUri.CREATE_COMMENT; import static com.yam.app.comment.presentation.CommentApiUri.DELETE_COMMENT; +import static com.yam.app.comment.presentation.CommentApiUri.FIND_BY_ARTICLE_ID; import static com.yam.app.comment.presentation.CommentApiUri.UPDATE_COMMENT; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.yam.app.account.presentation.LoginAccountCommand; @@ -124,4 +127,49 @@ final class CommentIntegrationTests extends AbstractIntegrationTests { .andExpect(status().isOk()); }); } + + @Test + @DisplayName("로그인에 적절한 파라미터를 입력하여 성공하고" + + " 인증된 사용자가 유효한 게시글, 유효하지 않는 게시글의 댓글을 각각 조회하는 시나리오 테스트") + void login_success_and_get_comment_by_article_id_scenarios() throws Exception { + //Arrange + var loginCommand = new LoginAccountCommand(); + loginCommand.setEmail("comment@gmail.com"); + loginCommand.setPassword("password!"); + + var articleId = 1L; + var invalidArticleId = 9999L; + + //Act & Assert + mockMvc.perform(post(LOGIN) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(loginCommand)) + ) + .andExpect(status().isOk()) + .andDo( + result -> { + final var actions = mockMvc.perform(get(FIND_BY_ARTICLE_ID + articleId) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + ); + + actions + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data").isArray()); + }) + .andDo( + result -> { + final var actions = mockMvc.perform(get(FIND_BY_ARTICLE_ID + invalidArticleId) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + ); + + actions + .andDo(print()) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.data").doesNotExist()); + }); + } }