Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Comment API에서 도메인명 안바뀐 부분 수정 #609 #610

Merged
merged 4 commits into from
Feb 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import com.staccato.comment.controller.docs.CommentControllerDocs;
import com.staccato.comment.service.CommentService;
import com.staccato.comment.service.dto.request.CommentRequest;
import com.staccato.comment.service.dto.request.CommentRequestV2;
import com.staccato.comment.service.dto.request.CommentUpdateRequest;
import com.staccato.comment.service.dto.response.CommentResponses;
import com.staccato.config.auth.LoginMember;
Expand All @@ -42,6 +43,23 @@ public ResponseEntity<Void> createComment(
.build();
}

@PostMapping("/v2")
public ResponseEntity<Void> createComment(
@LoginMember Member member,
@Valid @RequestBody CommentRequestV2 commentRequestV2
) {
long commentId = commentService.createComment(toCommentRequest(commentRequestV2), member);
return ResponseEntity.created(URI.create("/comments/" + commentId))
.build();
}

private CommentRequest toCommentRequest(CommentRequestV2 commentRequestV2) {
return new CommentRequest(
commentRequestV2.staccatoId(),
commentRequestV2.content()
);
}

@GetMapping
public ResponseEntity<CommentResponses> readCommentsByMomentId(
@LoginMember Member member,
Expand All @@ -51,6 +69,15 @@ public ResponseEntity<CommentResponses> readCommentsByMomentId(
return ResponseEntity.ok().body(commentResponses);
}

@GetMapping("/v2")
public ResponseEntity<CommentResponses> readCommentsByStaccatoId(
@LoginMember Member member,
@RequestParam @Min(value = 1L, message = "스타카토 식별자는 양수로 이루어져야 합니다.") long staccatoId
) {
CommentResponses commentResponses = commentService.readAllCommentsByMomentId(member, staccatoId);
return ResponseEntity.ok().body(commentResponses);
}

@PutMapping("/{commentId}")
public ResponseEntity<Void> updateComment(
@LoginMember Member member,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.staccato.comment.service.dto.request;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

import com.staccato.comment.domain.Comment;
import com.staccato.member.domain.Member;
import com.staccato.moment.domain.Moment;

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "댓글 생성 시 요청 형식입니다.")
public record CommentRequestV2(
@Schema(example = "1")
@NotNull(message = "스타카토를 선택해주세요.")
@Min(value = 1L, message = "스타카토 식별자는 양수로 이루어져야 합니다.")
Long staccatoId,
@Schema(example = "예시 댓글 내용")
@NotBlank(message = "댓글 내용을 입력해주세요.")
@Size(max = 500, message = "댓글은 공백 포함 500자 이하로 입력해주세요.")
String content
) {
public Comment toComment(Moment moment, Member member) {
return Comment.builder()
.content(content)
.moment(moment)
.member(member)
.build();
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테스트는 하나의 파일에서 만들려다가 테스트 데이터 구성에 복잡함을 느껴서, 구현의 편리성을 위해 다른 파일로 분리해서 작업했습니다.

해당 테스트 파일에 기존 컨트롤로 테스트가 전부 있는게 아닌 변경에 대한 테스트만 일부 있군요! 처음에 얼핏 봤을 때는 모든 엔드포인트 테스트가 있을거라고 생각이 들었던 것 같아요. 또한, v2를 제거할땐 해당 테스트파일과 기존 컨트롤러 테스트 파일 총 2 군데 변경 지점이 생겨서 다른 개발자가 추후에 수정하게 된다면 헷갈릴 여지가 있다고 생각이 들었어요!
그래서 한 가지 제안 드리자면, 기존에 있는 관련 테스트에 필요한 요청 데이터와 검증부만 추가하는건 어떻게 생각하시나요?
제가 전에 썼던 방식이기도 한데요, 변경 지점이 하나로 분명하니까 편리하지 않을까 싶어서 제안드립니당

Copy link
Contributor Author

@BurningFalls BurningFalls Feb 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 테스트 파일에 기존 컨트롤로 테스트가 전부 있는게 아닌 변경에 대한 테스트만 일부 있군요! 처음에 얼핏 봤을 때는 모든 엔드포인트 테스트가 있을거라고 생각이 들었던 것 같아요. 또한, v2를 제거할땐 해당 테스트파일과 기존 컨트롤러 테스트 파일 총 2 군데 변경 지점이 생겨서 다른 개발자가 추후에 수정하게 된다면 헷갈릴 여지가 있다고 생각이 들었어요!

결국에 나중에 수정할 때, (새로운 파일을 살리는 경우) 새로운 파일에 원래 테스트를 넣어야 하므로, 지금 추가해놓는 게 낫겠네요! 수정했습니다.

그래서 한 가지 제안 드리자면, 기존에 있는 관련 테스트에 필요한 요청 데이터와 검증부만 추가하는건 어떻게 생각하시나요?
제가 전에 썼던 방식이기도 한데요, 변경 지점이 하나로 분명하니까 편리하지 않을까 싶어서 제안드립니당

이 방법을 시도해보려 했으나, createComment에서는 괜찮은데, createCommentFail에는 MethodSource를 사용하기 때문에 하나의 함수에서 처리하지 못하고, readCommentsByMomentIdreadCommentsByMomentIdFail는 메서드 이름의 MomentIdStaccatoId로 바뀌어야 하기 때문에, 해당 방법을 사용하지 않았습니다.

그리고 이 새로 생겨난 메서드들까지 원래 파일에서 관리하면 오히려 나중에 제거할때 훨씬 복잡해질 것 같아, 새로운 파일로 분리해서 만들었습니다. 혹시 제가 생각하지 못한 더 좋은 방법이 있다면, 공유 부탁드립니다!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 방법을 시도해보려 했으나, createComment에서는 괜찮은데, createCommentFail에는 MethodSource를 사용하기 때문에 하나의 함수에서 처리하지 못하고, readCommentsByMomentId와 readCommentsByMomentIdFail는 메서드 이름의 MomentId가 StaccatoId로 바뀌어야 하기 때문에, 해당 방법을 사용하지 않았습니다.

그렇겠네요! 두 메서드는 어쩔 수 없을 것 같아요! 반영하시느라 수고하셨슴다~!~!

Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
package com.staccato.comment.controller;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
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.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import java.util.List;
import java.util.stream.Stream;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.NullSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

import com.staccato.ControllerTest;
import com.staccato.comment.service.dto.request.CommentRequestV2;
import com.staccato.comment.service.dto.request.CommentUpdateRequest;
import com.staccato.comment.service.dto.response.CommentResponse;
import com.staccato.comment.service.dto.response.CommentResponses;
import com.staccato.exception.ExceptionResponse;
import com.staccato.fixture.Member.MemberFixture;
import com.staccato.fixture.comment.CommentUpdateRequestFixture;

public class CommentControllerV2Test extends ControllerTest {
private static final int MAX_CONTENT_LENGTH = 500;
private static final long MIN_STACCATO_ID = 1L;

static Stream<Arguments> invalidCommentRequestProvider() {
return Stream.of(
Arguments.of(
new CommentRequestV2(null, "예시 댓글 내용"),
"스타카토를 선택해주세요."
),
Arguments.of(
new CommentRequestV2(MIN_STACCATO_ID - 1, "예시 댓글 내용"),
"스타카토 식별자는 양수로 이루어져야 합니다."
),
Arguments.of(
new CommentRequestV2(MIN_STACCATO_ID, null),
"댓글 내용을 입력해주세요."
),
Arguments.of(
new CommentRequestV2(MIN_STACCATO_ID, ""),
"댓글 내용을 입력해주세요."
),
Arguments.of(
new CommentRequestV2(MIN_STACCATO_ID, " "),
"댓글 내용을 입력해주세요."
),
Arguments.of(
new CommentRequestV2(MIN_STACCATO_ID, "1".repeat(MAX_CONTENT_LENGTH + 1)),
"댓글은 공백 포함 500자 이하로 입력해주세요."
)
);
}

@DisplayName("댓글 생성 요청/응답에 대한 직렬화/역직렬화에 성공한다.")
@Test
void createComment() throws Exception {
// given
when(authService.extractFromToken(any())).thenReturn(MemberFixture.create());
String commentRequest = """
{
"staccatoId": 1,
"content": "content"
}
""";
when(commentService.createComment(any(), any())).thenReturn(1L);

// when & then
mockMvc.perform(post("/comments/v2")
.contentType(MediaType.APPLICATION_JSON)
.content(commentRequest)
.header(HttpHeaders.AUTHORIZATION, "token"))
.andExpect(status().isCreated())
.andExpect(header().string(HttpHeaders.LOCATION, "/comments/1"));
}

@DisplayName("올바르지 않은 형식으로 정보를 입력하면, 댓글을 생성할 수 없다.")
@ParameterizedTest
@MethodSource("invalidCommentRequestProvider")
void createCommentFail(CommentRequestV2 commentRequestV2, String expectedMessage) throws Exception {
// given
when(authService.extractFromToken(any())).thenReturn(MemberFixture.create());
ExceptionResponse exceptionResponse = new ExceptionResponse(HttpStatus.BAD_REQUEST.toString(), expectedMessage);

// when & then
mockMvc.perform(post("/comments/v2")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(commentRequestV2))
.header(HttpHeaders.AUTHORIZATION, "token"))
.andExpect(status().isBadRequest())
.andExpect(content().json(objectMapper.writeValueAsString(exceptionResponse)));
}

@DisplayName("댓글을 조회했을 때 응답 직렬화에 성공한다.")
@Test
void readCommentsByStaccatoId() throws Exception {
// given
when(authService.extractFromToken(any())).thenReturn(MemberFixture.create());
CommentResponse commentResponse = new CommentResponse(1L, 1L, "member", "image.jpg", "내용");
CommentResponses commentResponses = new CommentResponses(List.of(commentResponse));
when(commentService.readAllCommentsByMomentId(any(), any())).thenReturn(commentResponses);
String expectedResponse = """
{
"comments": [
{
"commentId": 1,
"memberId": 1,
"nickname": "member",
"memberImageUrl": "image.jpg",
"content": "내용"
}
]
}
""";

// when & then
mockMvc.perform(get("/comments/v2")
.param("staccatoId", "1")
.contentType(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "token"))
.andExpect(status().isOk())
.andExpect(content().json(expectedResponse));
}

@DisplayName("스타카토 식별자가 양수가 아닐 경우 댓글 읽기에 실패한다.")
@Test
void readCommentsByStaccatoIdFail() throws Exception {
// given
when(authService.extractFromToken(any())).thenReturn(MemberFixture.create());
ExceptionResponse exceptionResponse = new ExceptionResponse(HttpStatus.BAD_REQUEST.toString(), "스타카토 식별자는 양수로 이루어져야 합니다.");

// when & then
mockMvc.perform(get("/comments/v2")
.param("staccatoId", "0")
.contentType(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "token"))
.andExpect(status().isBadRequest())
.andExpect(content().json(objectMapper.writeValueAsString(exceptionResponse)));
}

@DisplayName("댓글 수정 요청 역직렬화에 성공한다.")
@Test
void updateComment() throws Exception {
// given
when(authService.extractFromToken(any())).thenReturn(MemberFixture.create());
String commentUpdateRequest = """
{
"content": "content"
}
""";

// when & then
mockMvc.perform(put("/comments/{commentId}", 1)
.content(commentUpdateRequest)
.contentType(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "token"))
.andExpect(status().isOk());
}

@DisplayName("댓글 식별자가 양수가 아닐 경우 댓글 수정에 실패한다.")
@Test
void updateCommentFail() throws Exception {
// given
when(authService.extractFromToken(any())).thenReturn(MemberFixture.create());
CommentUpdateRequest commentUpdateRequest = CommentUpdateRequestFixture.create();
ExceptionResponse exceptionResponse = new ExceptionResponse(HttpStatus.BAD_REQUEST.toString(), "댓글 식별자는 양수로 이루어져야 합니다.");

// when & then
mockMvc.perform(put("/comments/{commentId}", 0)
.content(objectMapper.writeValueAsString(commentUpdateRequest))
.contentType(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "token"))
.andExpect(status().isBadRequest())
.andExpect(content().json(objectMapper.writeValueAsString(exceptionResponse)));
}

@DisplayName("댓글 내용을 입력하지 않을 경우 댓글 수정에 실패한다.")
@ParameterizedTest
@NullSource
@ValueSource(strings = {"", " "})
void updateCommentFailByBlank(String updatedContent) throws Exception {
// given
when(authService.extractFromToken(any())).thenReturn(MemberFixture.create());
CommentUpdateRequest commentUpdateRequest = new CommentUpdateRequest(updatedContent);
ExceptionResponse exceptionResponse = new ExceptionResponse(HttpStatus.BAD_REQUEST.toString(), "댓글 내용을 입력해주세요.");

// when & then
mockMvc.perform(put("/comments/{commentId}", 1)
.content(objectMapper.writeValueAsString(commentUpdateRequest))
.contentType(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "token"))
.andExpect(status().isBadRequest())
.andExpect(content().json(objectMapper.writeValueAsString(exceptionResponse)));
}

@DisplayName("올바른 형식으로 댓글 삭제를 시도하면 성공한다.")
@Test
void deleteComment() throws Exception {
// given
when(authService.extractFromToken(any())).thenReturn(MemberFixture.create());

// when & then
mockMvc.perform(delete("/comments/{commentId}", 1)
.contentType(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "token"))
.andExpect(status().isOk());
}

@DisplayName("댓글 식별자가 양수가 아닐 경우 댓글 삭제에 실패한다.")
@Test
void deleteCommentFail() throws Exception {
// given
when(authService.extractFromToken(any())).thenReturn(MemberFixture.create());
ExceptionResponse exceptionResponse = new ExceptionResponse(HttpStatus.BAD_REQUEST.toString(), "댓글 식별자는 양수로 이루어져야 합니다.");

// when & then
mockMvc.perform(delete("/comments/{commentId}", 0)
.contentType(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "token"))
.andExpect(status().isBadRequest())
.andExpect(content().json(objectMapper.writeValueAsString(exceptionResponse)));
}
}