diff --git a/build.gradle b/build.gradle index 81bb93e739..041db8f2c5 100644 --- a/build.gradle +++ b/build.gradle @@ -17,6 +17,7 @@ dependencies { runtimeOnly 'com.h2database:h2' testImplementation 'org.assertj:assertj-core:3.22.0' testImplementation 'org.springframework.boot:spring-boot-starter-test' + implementation 'org.springframework.boot:spring-boot-starter-web' } tasks.named('test') { diff --git a/src/main/java/qna/CannotDeleteException.java b/src/main/java/qna/CannotDeleteException.java index 12ea9bc148..6e4897d025 100644 --- a/src/main/java/qna/CannotDeleteException.java +++ b/src/main/java/qna/CannotDeleteException.java @@ -1,9 +1,9 @@ package qna; -public class CannotDeleteException extends Exception { +public class CannotDeleteException extends RuntimeException { private static final long serialVersionUID = 1L; public CannotDeleteException(String message) { super(message); } -} \ No newline at end of file +} diff --git a/src/main/java/qna/domain/Answer.java b/src/main/java/qna/domain/Answer.java index 548b71ed71..fd3ede15bb 100644 --- a/src/main/java/qna/domain/Answer.java +++ b/src/main/java/qna/domain/Answer.java @@ -1,5 +1,6 @@ package qna.domain; +import qna.CannotDeleteException; import qna.NotFoundException; import qna.UnAuthorizedException; @@ -43,9 +44,11 @@ public Answer(Long id, User writer, Question question, String contents) { this.contents = contents; } - public Answer setDeleted(boolean deleted) { - this.deleted = deleted; - return this; + public void delete(final User loginUser) { + if (!this.writer.equals(loginUser)) { + throw new CannotDeleteException("다른 사람이 쓴 답변이 있어 삭제할 수 없습니다."); + } + this.deleted = true; } public boolean isDeleted() { diff --git a/src/main/java/qna/domain/AnswerList.java b/src/main/java/qna/domain/AnswerList.java new file mode 100644 index 0000000000..22a244063e --- /dev/null +++ b/src/main/java/qna/domain/AnswerList.java @@ -0,0 +1,35 @@ +package qna.domain; + +import java.util.ArrayList; +import java.util.List; + +import javax.persistence.CascadeType; +import javax.persistence.Embeddable; +import javax.persistence.OneToMany; +import javax.persistence.OrderBy; + +import org.hibernate.annotations.Where; + +@Embeddable +public class AnswerList { + @OneToMany(mappedBy = "question", cascade = CascadeType.ALL) + @Where(clause = "deleted = false") + @OrderBy("id ASC") + private final List answers = new ArrayList<>(); + + public boolean isEmpty() { + return answers.isEmpty(); + } + + public void add(final Answer answer) { + answers.add(answer); + } + + public void deleteAll(final User loginUser) { + answers.forEach(answer -> answer.delete(loginUser)); + } + + public List getAnswers() { + return answers; + } +} diff --git a/src/main/java/qna/domain/DeleteEventHandler.java b/src/main/java/qna/domain/DeleteEventHandler.java new file mode 100644 index 0000000000..216b253415 --- /dev/null +++ b/src/main/java/qna/domain/DeleteEventHandler.java @@ -0,0 +1,33 @@ +package qna.domain; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import javax.annotation.Resource; + +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import qna.service.DeleteHistoryService; + +@Component +public class DeleteEventHandler { + + @Resource(name = "deleteHistoryService") + private DeleteHistoryService deleteHistoryService; + + @EventListener + public void saveHistory(DeleteQuestionEvent deleteQuestionEvent) { + System.out.println("Subscribe deleteQuestion event!!!!!"); + final Question question = deleteQuestionEvent.getQuestion(); + + List deleteHistories = new ArrayList<>(); + deleteHistories.add(new DeleteHistory(ContentType.QUESTION, question.getId(), question.getWriter(), LocalDateTime.now())); + + for (Answer answer : question.getAnswers().getAnswers()) { + deleteHistories.add(new DeleteHistory(ContentType.ANSWER, answer.getId(), answer.getWriter(), LocalDateTime.now())); + } + deleteHistoryService.saveAll(deleteHistories); + } +} diff --git a/src/main/java/qna/domain/DeleteQuestionEvent.java b/src/main/java/qna/domain/DeleteQuestionEvent.java new file mode 100644 index 0000000000..15a1cfdd9e --- /dev/null +++ b/src/main/java/qna/domain/DeleteQuestionEvent.java @@ -0,0 +1,13 @@ +package qna.domain; + +public class DeleteQuestionEvent { + private final Question question; + + public DeleteQuestionEvent(final Question question) { + this.question = question; + } + + public Question getQuestion() { + return question; + } +} diff --git a/src/main/java/qna/domain/Question.java b/src/main/java/qna/domain/Question.java index 1e8bb11251..7407238fcf 100644 --- a/src/main/java/qna/domain/Question.java +++ b/src/main/java/qna/domain/Question.java @@ -1,10 +1,14 @@ package qna.domain; -import org.hibernate.annotations.Where; +import javax.persistence.Column; +import javax.persistence.Embedded; +import javax.persistence.Entity; +import javax.persistence.ForeignKey; +import javax.persistence.JoinColumn; +import javax.persistence.Lob; +import javax.persistence.ManyToOne; -import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; +import qna.CannotDeleteException; @Entity public class Question extends AbstractEntity { @@ -18,10 +22,8 @@ public class Question extends AbstractEntity { @JoinColumn(foreignKey = @ForeignKey(name = "fk_question_writer")) private User writer; - @OneToMany(mappedBy = "question", cascade = CascadeType.ALL) - @Where(clause = "deleted = false") - @OrderBy("id ASC") - private List answers = new ArrayList<>(); + @Embedded + private final AnswerList answers = new AnswerList(); private boolean deleted = false; @@ -80,11 +82,26 @@ public Question setDeleted(boolean deleted) { return this; } + public void delete(final User loginUser) throws CannotDeleteException { + if (!this.writer.equals(loginUser)) { + throw new CannotDeleteException("질문자 본인만 삭제할 수 있습니다."); + } + + if (answers.isEmpty()) { + this.deleted = true; + return; + } + + answers.deleteAll(loginUser); + + this.deleted = true; + } + public boolean isDeleted() { return deleted; } - public List getAnswers() { + public AnswerList getAnswers() { return answers; } diff --git a/src/main/java/qna/service/QnAService.java b/src/main/java/qna/service/QnAService.java index 66821cd9c2..a8a1319073 100644 --- a/src/main/java/qna/service/QnAService.java +++ b/src/main/java/qna/service/QnAService.java @@ -2,6 +2,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import qna.CannotDeleteException; @@ -9,9 +10,6 @@ import qna.domain.*; import javax.annotation.Resource; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; @Service("qnaService") public class QnAService { @@ -20,11 +18,8 @@ public class QnAService { @Resource(name = "questionRepository") private QuestionRepository questionRepository; - @Resource(name = "answerRepository") - private AnswerRepository answerRepository; - - @Resource(name = "deleteHistoryService") - private DeleteHistoryService deleteHistoryService; + @Resource(name = "applicationEventPublisher") + private ApplicationEventPublisher applicationEventPublisher; @Transactional(readOnly = true) public Question findQuestionById(Long id) { @@ -35,24 +30,8 @@ public Question findQuestionById(Long id) { @Transactional public void deleteQuestion(User loginUser, long questionId) throws CannotDeleteException { Question question = findQuestionById(questionId); - if (!question.isOwner(loginUser)) { - throw new CannotDeleteException("질문을 삭제할 권한이 없습니다."); - } - - List answers = question.getAnswers(); - for (Answer answer : answers) { - if (!answer.isOwner(loginUser)) { - throw new CannotDeleteException("다른 사람이 쓴 답변이 있어 삭제할 수 없습니다."); - } - } - - List deleteHistories = new ArrayList<>(); - question.setDeleted(true); - deleteHistories.add(new DeleteHistory(ContentType.QUESTION, questionId, question.getWriter(), LocalDateTime.now())); - for (Answer answer : answers) { - answer.setDeleted(true); - deleteHistories.add(new DeleteHistory(ContentType.ANSWER, answer.getId(), answer.getWriter(), LocalDateTime.now())); - } - deleteHistoryService.saveAll(deleteHistories); + question.delete(loginUser); + + applicationEventPublisher.publishEvent(new DeleteQuestionEvent(question)); } } diff --git a/src/test/java/qna/domain/AnswerTest.java b/src/test/java/qna/domain/AnswerTest.java index d858181e31..c0209f1212 100644 --- a/src/test/java/qna/domain/AnswerTest.java +++ b/src/test/java/qna/domain/AnswerTest.java @@ -1,6 +1,45 @@ package qna.domain; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import qna.CannotDeleteException; + public class AnswerTest { public static final Answer A1 = new Answer(UserTest.JAVAJIGI, QuestionTest.Q1, "Answers Contents1"); public static final Answer A2 = new Answer(UserTest.SANJIGI, QuestionTest.Q1, "Answers Contents2"); + + private Answer answer; + + @BeforeEach + void setUp() { + answer = new Answer(11L, UserTest.JAVAJIGI, QuestionTest.Q1, "Answers Contents1"); + } + + @DisplayName("작성자가 본인이면 답변을 지울 수 있다.") + @Test + public void delete_success_owner() { + assertThat(answer.isDeleted()).isFalse(); + + answer.delete(UserTest.JAVAJIGI); + + assertThat(answer.isDeleted()).isTrue(); + } + + @DisplayName("작성자가 아니면 답변을 지울 수 없다.") + @Test + public void delete_fail_no_owner() { + assertThat(answer.isDeleted()).isFalse(); + + assertThatThrownBy(() -> answer.delete(UserTest.SANJIGI)) + .isInstanceOf(CannotDeleteException.class) + .hasMessage("다른 사람이 쓴 답변이 있어 삭제할 수 없습니다."); + + assertThat(answer.isDeleted()).isFalse(); + } + } diff --git a/src/test/java/qna/domain/QuestionTest.java b/src/test/java/qna/domain/QuestionTest.java index b48c9a2209..27a6f87d3f 100644 --- a/src/test/java/qna/domain/QuestionTest.java +++ b/src/test/java/qna/domain/QuestionTest.java @@ -1,6 +1,88 @@ package qna.domain; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import qna.CannotDeleteException; + public class QuestionTest { public static final Question Q1 = new Question("title1", "contents1").writeBy(UserTest.JAVAJIGI); public static final Question Q2 = new Question("title2", "contents2").writeBy(UserTest.SANJIGI); + + private Question question; + private Answer answer; + private Answer answer2; + + @BeforeEach + public void setUp() { + question = new Question(1L, "title1", "contents1").writeBy(UserTest.JAVAJIGI); + answer = new Answer(11L, UserTest.JAVAJIGI, QuestionTest.Q1, "Answers Contents1"); + answer2 = new Answer(UserTest.JAVAJIGI, question, "answer contents2"); + question.addAnswer(answer); + } + + + @DisplayName("질문자가 본인이 아니면 질문을 삭제할 수 없다.") + @Test + public void delete_fail_not_owner() { + assertThatThrownBy(() -> question.delete(UserTest.SANJIGI)) + .isInstanceOf(CannotDeleteException.class); + } + + @DisplayName("질문자 본인이고, 답변이 없으면 삭제가 가능하다.") + @Test + public void delete_success_owner_and_no_answer() { + assertThat(question.isDeleted()).isFalse(); + + question.delete(UserTest.JAVAJIGI); + + assertThat(question.isDeleted()).isTrue(); + } + + @DisplayName("질문자가 본인이고, 답변이 있는 경우, 모든 답변자가 본인이면 삭제가 가능하다.") + @Test + public void delete_success_question_and_answer_owner() { + question.addAnswer(answer); + assertThat(question.isDeleted()).isFalse(); + + question.delete(UserTest.JAVAJIGI); + + assertThat(question.isDeleted()).isTrue(); + } + + @DisplayName("질문자가 본인이고, 답변이 있는 경우, 모든 답변자가 본인이 아니면 삭제가 불가능하다.") + @Test + public void delete_fail_answer_other() { + question.addAnswer(AnswerTest.A1); + question.addAnswer(AnswerTest.A2); + + assertThatThrownBy(() -> question.delete(UserTest.JAVAJIGI)) + .isInstanceOf(CannotDeleteException.class) + .hasMessage("다른 사람이 쓴 답변이 있어 삭제할 수 없습니다."); + } + + @DisplayName("질문이 삭제되면 답변도 모두 삭제된다.") + @Test + public void delete_success_all_answer_deleted() { + question.addAnswer(answer2); + assertAll( + () -> assertThat(question.isDeleted()).isFalse(), + () -> assertThat(answer.isDeleted()).isFalse(), + () -> assertThat(answer2.isDeleted()).isFalse() + ); + + question.delete(UserTest.JAVAJIGI); + + assertAll( + () -> assertThat(question.isDeleted()).isTrue(), + () -> assertThat(answer.isDeleted()).isTrue(), + () -> assertThat(answer2.isDeleted()).isTrue() + ); + } + } diff --git a/src/test/java/qna/service/QnaServiceTest.java b/src/test/java/qna/service/QnaServiceTest.java index 3504343caa..17394a79f3 100644 --- a/src/test/java/qna/service/QnaServiceTest.java +++ b/src/test/java/qna/service/QnaServiceTest.java @@ -1,89 +1,111 @@ package qna.service; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Optional; + import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import qna.CannotDeleteException; -import qna.domain.*; - -import java.time.LocalDateTime; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; +import org.springframework.context.ApplicationEventPublisher; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import qna.CannotDeleteException; +import qna.domain.Answer; +import qna.domain.AnswerTest; +import qna.domain.DeleteQuestionEvent; +import qna.domain.Question; +import qna.domain.QuestionRepository; +import qna.domain.QuestionTest; +import qna.domain.UserTest; @ExtendWith(MockitoExtension.class) public class QnaServiceTest { @Mock private QuestionRepository questionRepository; - @Mock - private DeleteHistoryService deleteHistoryService; - @InjectMocks private QnAService qnAService; + @Mock + private ApplicationEventPublisher applicationEventPublisher; + private Question question; private Answer answer; @BeforeEach - public void setUp() throws Exception { + public void setUp() { question = new Question(1L, "title1", "contents1").writeBy(UserTest.JAVAJIGI); answer = new Answer(11L, UserTest.JAVAJIGI, QuestionTest.Q1, "Answers Contents1"); question.addAnswer(answer); } @Test - public void delete_성공() throws Exception { + public void delete_성공() { when(questionRepository.findByIdAndDeletedFalse(question.getId())).thenReturn(Optional.of(question)); assertThat(question.isDeleted()).isFalse(); qnAService.deleteQuestion(UserTest.JAVAJIGI, question.getId()); assertThat(question.isDeleted()).isTrue(); - verifyDeleteHistories(); + verify(applicationEventPublisher, times(1)).publishEvent(any(DeleteQuestionEvent.class)); } + @DisplayName("질문이 삭제되면 답변도 모두 삭제된다.") + @Test + public void delete_success_all_answer_deleted() { + final Answer answer01 = new Answer(UserTest.JAVAJIGI, question, "answer contents1"); + final Answer answer02 = new Answer(UserTest.JAVAJIGI, question, "answer contents2"); + question.addAnswer(answer01); + question.addAnswer(answer02); + when(questionRepository.findByIdAndDeletedFalse(question.getId())).thenReturn(Optional.of(question)); + + qnAService.deleteQuestion(UserTest.JAVAJIGI, question.getId()); + + assertAll( + () -> assertThat(question.isDeleted()).isTrue(), + () -> assertThat(answer01.isDeleted()).isTrue(), + () -> assertThat(answer02.isDeleted()).isTrue() + ); + } @Test - public void delete_다른_사람이_쓴_글() throws Exception { + public void delete_실패_다른_사람이_쓴_글() { when(questionRepository.findByIdAndDeletedFalse(question.getId())).thenReturn(Optional.of(question)); assertThatThrownBy(() -> { qnAService.deleteQuestion(UserTest.SANJIGI, question.getId()); - }).isInstanceOf(CannotDeleteException.class); + }).isInstanceOf(CannotDeleteException.class) + .hasMessage("질문자 본인만 삭제할 수 있습니다."); } @Test - public void delete_성공_질문자_답변자_같음() throws Exception { + public void delete_성공_질문자_답변자_같음() { when(questionRepository.findByIdAndDeletedFalse(question.getId())).thenReturn(Optional.of(question)); qnAService.deleteQuestion(UserTest.JAVAJIGI, question.getId()); assertThat(question.isDeleted()).isTrue(); assertThat(answer.isDeleted()).isTrue(); - verifyDeleteHistories(); + verify(applicationEventPublisher, times(1)).publishEvent(any(DeleteQuestionEvent.class)); } @Test - public void delete_답변_중_다른_사람이_쓴_글() throws Exception { + public void delete_실패_답변_중_다른_사람이_쓴_글() { + question.addAnswer(AnswerTest.A2); when(questionRepository.findByIdAndDeletedFalse(question.getId())).thenReturn(Optional.of(question)); assertThatThrownBy(() -> { - qnAService.deleteQuestion(UserTest.SANJIGI, question.getId()); - }).isInstanceOf(CannotDeleteException.class); - } - - private void verifyDeleteHistories() { - List deleteHistories = Arrays.asList( - new DeleteHistory(ContentType.QUESTION, question.getId(), question.getWriter(), LocalDateTime.now()), - new DeleteHistory(ContentType.ANSWER, answer.getId(), answer.getWriter(), LocalDateTime.now())); - verify(deleteHistoryService).saveAll(deleteHistories); + qnAService.deleteQuestion(UserTest.JAVAJIGI, question.getId()); + }).isInstanceOf(CannotDeleteException.class) + .hasMessage("다른 사람이 쓴 답변이 있어 삭제할 수 없습니다."); } }