Add comment query

게시글 댓글 조회 추가
테스트 추가
This commit is contained in:
JiwonDev
2021-10-22 02:47:14 +09:00
committed by Jiwon
parent 3af4ef973f
commit e88bad95df
9 changed files with 265 additions and 6 deletions

View File

@@ -1,8 +1,17 @@
package com.yam.app.comment.application; 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.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.CreateCommentCommand;
import com.yam.app.comment.presentation.UpdateCommentCommand; 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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -10,9 +19,17 @@ import org.springframework.transaction.annotation.Transactional;
public class CommentFacade { public class CommentFacade {
private final CommentProcessor commentProcessor; 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.commentProcessor = commentProcessor;
this.articleReader = articleReader;
this.commentReader = commentReader;
this.memberReader = memberReader;
} }
@Transactional @Transactional
@@ -29,4 +46,35 @@ public class CommentFacade {
public void delete(Long commentId, Long memberId) { public void delete(Long commentId, Long memberId) {
commentProcessor.delete(commentId, memberId); commentProcessor.delete(commentId, memberId);
} }
@Transactional(readOnly = true)
public List<CommentResponse> 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());
}
} }

View File

@@ -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<ApiResult<List<CommentResponse>>> getComments(
@PathVariable Long articleId) {
return ResponseEntity.ok(
ApiResult.success(commentFacade.findByArticleId(articleId)));
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -15,7 +15,7 @@ import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
@DisplayName("Article Qurey HTTP API") @DisplayName("Article Query HTTP API")
@WebMvcTest(ArticleQueryApi.class) @WebMvcTest(ArticleQueryApi.class)
@ActiveProfiles("test") @ActiveProfiles("test")
class ArticleQueryApiTest { class ArticleQueryApiTest {

View File

@@ -5,6 +5,7 @@ public final class CommentApiUri {
public static final String CREATE_COMMENT = "/api/comments/"; public static final String CREATE_COMMENT = "/api/comments/";
public static final String UPDATE_COMMENT = "/api/comments/"; public static final String UPDATE_COMMENT = "/api/comments/";
public static final String DELETE_COMMENT = "/api/comments/"; public static final String DELETE_COMMENT = "/api/comments/";
public static final String FIND_BY_ARTICLE_ID = "/api/comments/";
private CommentApiUri() { private CommentApiUri() {
} }

View File

@@ -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"));
}
}

View File

@@ -2,10 +2,10 @@ package com.yam.app.integration;
import static com.yam.app.account.presentation.AccountApiUri.LOGIN; 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_ALL;
import static com.yam.app.article.presentation.ArticleApiUri.FIND_BY_ID;
import static com.yam.app.article.presentation.ArticleApiUri.WRITE_ARTICLE; 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.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 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.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 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 { void default_main_page_find_all_preview_article_response() throws Exception {
// Act // Act
mockMvc.perform(get(FIND_ALL) mockMvc.perform(get(FIND_ALL)
.accept(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
) )
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.data").isArray()); .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());
});
}
} }

View File

@@ -3,11 +3,14 @@ package com.yam.app.integration;
import static com.yam.app.account.presentation.AccountApiUri.LOGIN; 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.CREATE_COMMENT;
import static com.yam.app.comment.presentation.CommentApiUri.DELETE_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 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.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.patch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 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.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.yam.app.account.presentation.LoginAccountCommand; import com.yam.app.account.presentation.LoginAccountCommand;
@@ -124,4 +127,49 @@ final class CommentIntegrationTests extends AbstractIntegrationTests {
.andExpect(status().isOk()); .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());
});
}
} }