Skip to content

Commit

Permalink
댓글 삭제 기능 구현 (#161)
Browse files Browse the repository at this point in the history
* feat: (#131) 댓글 작성자, 게시글 검증 기능 구현

* feat: (#131) 댓글 삭제 기능 구현

* feat: (#131) 댓글 삭제 API 구현

* refactor: (#131) 댓글 삭제 문서 수정

* refactor: (#131) 인증된 멤버 매개변수 위치 수정

* fix: (#131) 게시글 변수명 수정에 따른 빌더 체이닝 메서드명 수정

* chore: (#131) 코드 컨벤션 정리
  • Loading branch information
woo-chang authored Jul 29, 2023
1 parent d762c15 commit 66c23c5
Show file tree
Hide file tree
Showing 8 changed files with 322 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand Down Expand Up @@ -49,4 +50,23 @@ public ResponseEntity<Void> createComment(
return ResponseEntity.status(HttpStatus.CREATED).build();
}

@Operation(summary = "게시글 댓글 삭제", description = "게시글 댓글을 삭제한다.")
@ApiResponses({
@ApiResponse(responseCode = "204", description = "게시글 댓글 삭제 성공"),
@ApiResponse(
responseCode = "404",
description = "존재하지 않는 게시글, 존재하지 않는 댓글",
content = @Content(schema = @Schema(implementation = ExceptionResponse.class))
)
})
@DeleteMapping("/{postId}/comments/{commentId}")
public ResponseEntity<Void> deleteComment(
@PathVariable @Parameter(description = "게시글 ID") final Long postId,
@PathVariable @Parameter(description = "댓글 ID") final Long commentId,
@Auth final Member member
) {
postCommentService.deleteComment(postId, commentId, member);
return ResponseEntity.noContent().build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.votogether.domain.common.BaseEntity;
import com.votogether.domain.member.entity.Member;
import com.votogether.domain.post.entity.Post;
import com.votogether.domain.post.exception.CommentExceptionType;
import com.votogether.exception.BadRequestException;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
Expand All @@ -11,6 +13,7 @@
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import java.util.Objects;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
Expand Down Expand Up @@ -45,4 +48,16 @@ private Comment(final Post post, final Member member, final String content) {
this.content = new Content(content);
}

public void validateWriter(final Member member) {
if (!Objects.equals(this.member.getId(), member.getId())) {
throw new BadRequestException(CommentExceptionType.NOT_WRITER);
}
}

public void validateBelong(final Post post) {
if (!Objects.equals(this.post.getId(), post.getId())) {
throw new BadRequestException(CommentExceptionType.NOT_BELONG_POST);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
@Getter
public enum CommentExceptionType implements ExceptionType {

INVALID_CONTENT_LENGTH(2000, "유효하지 않은 댓글 길이입니다.");
INVALID_CONTENT_LENGTH(2000, "유효하지 않은 댓글 길이입니다."),
COMMENT_NOT_FOUND(2001, "해당 댓글이 존재하지 않습니다."),
NOT_BELONG_POST(2002, "댓글의 게시글 정보와 일치하지 않습니다."),
NOT_WRITER(2003, "댓글 작성자가 아닙니다.");

private final int code;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import com.votogether.domain.post.dto.request.CommentRegisterRequest;
import com.votogether.domain.post.entity.Post;
import com.votogether.domain.post.entity.comment.Comment;
import com.votogether.domain.post.exception.CommentExceptionType;
import com.votogether.domain.post.exception.PostExceptionType;
import com.votogether.domain.post.repository.CommentRepository;
import com.votogether.domain.post.repository.PostRepository;
import com.votogether.exception.NotFoundException;
import lombok.RequiredArgsConstructor;
Expand All @@ -16,6 +18,7 @@
public class PostCommentService {

private final PostRepository postRepository;
private final CommentRepository commentRepository;

@Transactional
public void createComment(
Expand All @@ -34,4 +37,17 @@ public void createComment(
post.addComment(comment);
}

@Transactional
public void deleteComment(final Long postId, final Long commentId, final Member member) {
final Post post = postRepository.findById(postId)
.orElseThrow(() -> new NotFoundException(PostExceptionType.POST_NOT_FOUND));
final Comment comment = commentRepository.findById(commentId)
.orElseThrow(() -> new NotFoundException(CommentExceptionType.COMMENT_NOT_FOUND));

comment.validateBelong(post);
comment.validateWriter(member);

commentRepository.delete(comment);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willDoNothing;

import com.votogether.domain.member.entity.Member;
import com.votogether.domain.member.service.MemberService;
import com.votogether.domain.post.dto.request.CommentRegisterRequest;
import com.votogether.domain.post.service.PostCommentService;
Expand Down Expand Up @@ -40,7 +43,7 @@ class PostCommentControllerTest {
TokenProcessor tokenProcessor;

@BeforeEach
void setUp(final WebApplicationContext webApplicationContext) {
void setUp(WebApplicationContext webApplicationContext) {
RestAssuredMockMvc.standaloneSetup(new PostCommentController(postCommentService));
RestAssuredMockMvc.webAppContextSetup(webApplicationContext);
}
Expand Down Expand Up @@ -72,12 +75,13 @@ void invalidIDType(String id) throws Exception {
@ParameterizedTest
@NullAndEmptySource
@DisplayName("댓글 내용이 존재하지 않으면 400을 응답한다.")
void emptyContent(final String content) throws Exception {
void emptyContent(String content) throws Exception {
// given
TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L);
given(tokenProcessor.resolveToken(anyString())).willReturn("token");
given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload);
given(memberService.findById(anyLong())).willReturn(MemberFixtures.MALE_20.get());

CommentRegisterRequest commentRegisterRequest = new CommentRegisterRequest(content);

// when, then
Expand All @@ -100,7 +104,10 @@ void createComment() throws Exception {
given(tokenProcessor.resolveToken(anyString())).willReturn("token");
given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload);
given(memberService.findById(anyLong())).willReturn(MemberFixtures.MALE_20.get());

CommentRegisterRequest commentRegisterRequest = new CommentRegisterRequest("댓글입니다.");
willDoNothing().given(postCommentService)
.createComment(any(Member.class), anyLong(), any(CommentRegisterRequest.class));

// when, then
RestAssuredMockMvc.given().log().all()
Expand All @@ -114,4 +121,69 @@ void createComment() throws Exception {

}

@Nested
@DisplayName("게시글 댓글 삭제")
class DeleteComment {

@ParameterizedTest
@ValueSource(strings = {"@", "a", "가"})
@DisplayName("게시글 ID가 Long 타입으로 변환할 수 없는 값이라면 400을 응답한다.")
void invalidPostIDType(String postId) throws Exception {
// given
TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L);
given(tokenProcessor.resolveToken(anyString())).willReturn("token");
given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload);
given(memberService.findById(anyLong())).willReturn(MemberFixtures.MALE_20.get());

// when, then
RestAssuredMockMvc.given().log().all()
.headers(HttpHeaders.AUTHORIZATION, "Bearer token")
.when().delete("/posts/{postId}/comments/{commentId}", postId, 1L)
.then().log().all()
.status(HttpStatus.BAD_REQUEST)
.body("code", equalTo(-9998))
.body("message", containsString("postId는 Long 타입이 필요합니다."));
}

@ParameterizedTest
@ValueSource(strings = {"@", "a", "가"})
@DisplayName("댓글 ID가 Long 타입으로 변환할 수 없는 값이라면 400을 응답한다.")
void invalidCommentIDType(String commentId) throws Exception {
// given
TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L);
given(tokenProcessor.resolveToken(anyString())).willReturn("token");
given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload);
given(memberService.findById(anyLong())).willReturn(MemberFixtures.MALE_20.get());

// when, then
RestAssuredMockMvc.given().log().all()
.headers(HttpHeaders.AUTHORIZATION, "Bearer token")
.when().delete("/posts/{postId}/comments/{commentId}", 1L, commentId)
.then().log().all()
.status(HttpStatus.BAD_REQUEST)
.body("code", equalTo(-9998))
.body("message", containsString("commentId는 Long 타입이 필요합니다."));
}

@Test
@DisplayName("댓글을 정상적으로 삭제하면 204를 응답한다.")
void deleteComment() throws Exception {
// given
TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L);
given(tokenProcessor.resolveToken(anyString())).willReturn("token");
given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload);
given(memberService.findById(anyLong())).willReturn(MemberFixtures.MALE_20.get());

willDoNothing().given(postCommentService).deleteComment(anyLong(), anyLong(), any(Member.class));

// when, then
RestAssuredMockMvc.given().log().all()
.headers(HttpHeaders.AUTHORIZATION, "Bearer token")
.when().delete("/posts/{postId}/comments/{commentId}", 1L, 1L)
.then().log().all()
.status(HttpStatus.NO_CONTENT);
}

}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.votogether.domain.post.entity;

import static com.votogether.fixtures.MemberFixtures.FEMALE_20;
import static com.votogether.fixtures.MemberFixtures.MALE_30;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

import static org.assertj.core.api.Assertions.assertThatThrownBy;

import com.votogether.domain.member.entity.Member;
import com.votogether.domain.post.entity.Post;
import com.votogether.domain.post.entity.PostBody;
import com.votogether.exception.BadRequestException;
import com.votogether.fixtures.MemberFixtures;
import java.time.LocalDateTime;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;

class CommentTest {

Expand Down Expand Up @@ -39,4 +41,69 @@ void invalidContentLength() {
.hasMessage("유효하지 않은 댓글 길이입니다.");
}

@Test
@DisplayName("댓글 작성자가 아니라면 예외를 던진다.")
void invalidWriter() {
// given
Member member = MemberFixtures.FEMALE_20.get();
PostBody body = PostBody.builder()
.title("title")
.content("content")
.build();
Post post = Post.builder()
.writer(member)
.postBody(body)
.deadline(LocalDateTime.now())
.build();
Comment comment = Comment.builder()
.member(member)
.post(post)
.content("content")
.build();

ReflectionTestUtils.setField(member, "id", 1L);

// when, then
assertThatThrownBy(() -> comment.validateWriter(MemberFixtures.MALE_20.get()))
.isInstanceOf(BadRequestException.class)
.hasMessage("댓글 작성자가 아닙니다.");
}

@Test
@DisplayName("작성되어 있는 게시글이 아니라면 예외를 던진다.")
void invalidPost() {
// given
Member member = MemberFixtures.FEMALE_20.get();
PostBody bodyA = PostBody.builder()
.title("title")
.content("content")
.build();
Post postA = Post.builder()
.writer(member)
.postBody(bodyA)
.deadline(LocalDateTime.now())
.build();
PostBody bodyB = PostBody.builder()
.title("title")
.content("content")
.build();
Post postB = Post.builder()
.writer(member)
.postBody(bodyB)
.deadline(LocalDateTime.now())
.build();
Comment comment = Comment.builder()
.member(member)
.post(postA)
.content("content")
.build();

ReflectionTestUtils.setField(postA, "id", 1L);

// when, then
assertThatThrownBy(() -> comment.validateBelong(postB))
.isInstanceOf(BadRequestException.class)
.hasMessage("댓글의 게시글 정보와 일치하지 않습니다.");
}

}
Loading

0 comments on commit 66c23c5

Please sign in to comment.