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

댓글 삭제 기능 구현 #161

Merged
merged 8 commits into from
Jul 29, 2023
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);
Copy link
Collaborator

Choose a reason for hiding this comment

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

이런방법이 있었군요 하나 배워갑니다~


// 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