From a93937dadbd99078df35206cc067ae9fd2979faa Mon Sep 17 00:00:00 2001 From: kwonssshyeon <104684033+kwonssshyeon@users.noreply.github.com> Date: Tue, 22 Oct 2024 02:31:09 +0900 Subject: [PATCH] =?UTF-8?q?Test:=20=EC=A7=80=EC=9B=90=EC=84=9C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20/=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20(#126)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Test: 게시글 댓글 그룹 생성, 조회 쿼리 테스트 * Refac: Comment 테스트 코드 리팩토링 * Refac: 지원서 상태는 지원서 클래스에서 변경, transaction manager 빈 등록 후 재사용 * Test: 어드민 지원서 관련 복잡한 쿼리 테스트 코드 작성 * Fix: 새로 추가된 UNDEFINED 트랙을 응답에 포함하는 문제 수정 * Fix: 지원 상태 업데이트 조건문 변경 * Fix: + 연결자 대신 String formatter 변경 --- .../config/TxTemplateConfig.java | 15 +++ .../entity/application/Application.java | 10 +- .../entity/enumeration/Track.java | 12 +- .../repository/CommentRepository.java | 3 +- .../service/MailService.java | 2 +- .../admin/AdminApplicationService.java | 31 ++--- .../AdminApplicationRepositoryTest.java | 79 ++++++++++++ .../post/CommentRepositoryTest.java | 93 ++++++++++++++ .../service/admin/AdminApplicationTest.java | 117 ++++++++++++++++++ .../service/post/CommentServiceTest.java | 78 +++++------- 10 files changed, 368 insertions(+), 72 deletions(-) create mode 100644 src/main/java/com/gdsc_knu/official_homepage/config/TxTemplateConfig.java create mode 100644 src/test/java/com/gdsc_knu/official_homepage/repository/application/AdminApplicationRepositoryTest.java create mode 100644 src/test/java/com/gdsc_knu/official_homepage/repository/post/CommentRepositoryTest.java create mode 100644 src/test/java/com/gdsc_knu/official_homepage/service/admin/AdminApplicationTest.java diff --git a/src/main/java/com/gdsc_knu/official_homepage/config/TxTemplateConfig.java b/src/main/java/com/gdsc_knu/official_homepage/config/TxTemplateConfig.java new file mode 100644 index 00000000..a5a60505 --- /dev/null +++ b/src/main/java/com/gdsc_knu/official_homepage/config/TxTemplateConfig.java @@ -0,0 +1,15 @@ +package com.gdsc_knu.official_homepage.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; + +@Configuration +public class TxTemplateConfig { + public final TransactionTemplate transactionTemplate; + + public TxTemplateConfig(PlatformTransactionManager transactionManager) { + this.transactionTemplate = new TransactionTemplate(transactionManager); + } + +} diff --git a/src/main/java/com/gdsc_knu/official_homepage/entity/application/Application.java b/src/main/java/com/gdsc_knu/official_homepage/entity/application/Application.java index 95d1260f..fa4e03e6 100644 --- a/src/main/java/com/gdsc_knu/official_homepage/entity/application/Application.java +++ b/src/main/java/com/gdsc_knu/official_homepage/entity/application/Application.java @@ -117,12 +117,10 @@ public void changeMark() { this.isMarked = !this.isMarked; } - public void approve() { - this.applicationStatus = ApplicationStatus.APPROVED; - } - - public void reject() { - this.applicationStatus = ApplicationStatus.REJECTED; + public void updateStatus(ApplicationStatus status) { + if (this.applicationStatus != ApplicationStatus.TEMPORAL){ + this.applicationStatus = status; + } } public void saveNote(String note) { diff --git a/src/main/java/com/gdsc_knu/official_homepage/entity/enumeration/Track.java b/src/main/java/com/gdsc_knu/official_homepage/entity/enumeration/Track.java index 2612f793..05d570f2 100644 --- a/src/main/java/com/gdsc_knu/official_homepage/entity/enumeration/Track.java +++ b/src/main/java/com/gdsc_knu/official_homepage/entity/enumeration/Track.java @@ -1,5 +1,15 @@ package com.gdsc_knu.official_homepage.entity.enumeration; public enum Track { - FRONT_END, BACK_END, ANDROID, AI, DESIGNER, UNDEFINED + FRONT_END, BACK_END, ANDROID, AI, DESIGNER, UNDEFINED; + + public static Track[] getValidTrack() { + return new Track[] { + FRONT_END, + BACK_END, + ANDROID, + AI, + DESIGNER + }; + } } diff --git a/src/main/java/com/gdsc_knu/official_homepage/repository/CommentRepository.java b/src/main/java/com/gdsc_knu/official_homepage/repository/CommentRepository.java index 28094ee5..c64c3c1a 100644 --- a/src/main/java/com/gdsc_knu/official_homepage/repository/CommentRepository.java +++ b/src/main/java/com/gdsc_knu/official_homepage/repository/CommentRepository.java @@ -5,11 +5,12 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface CommentRepository extends JpaRepository { @Query("SELECT c " + "FROM Comment c " + "WHERE c.post.id = :postId "+ "ORDER BY c.parent.id ASC, c.createAt ASC") - Page findCommentAndReply(Pageable pageable, Long postId); + Page findCommentAndReply(Pageable pageable, @Param(value = "postId") Long postId); } diff --git a/src/main/java/com/gdsc_knu/official_homepage/service/MailService.java b/src/main/java/com/gdsc_knu/official_homepage/service/MailService.java index 477307bd..c474401d 100644 --- a/src/main/java/com/gdsc_knu/official_homepage/service/MailService.java +++ b/src/main/java/com/gdsc_knu/official_homepage/service/MailService.java @@ -75,7 +75,7 @@ private void sendMail(String email, String mailTemplate) { mailSender.send(message); } catch (MessagingException | MailException e) { redisRepository.addData(REDIS_KEY, email); - throw new CustomException(ErrorCode.FAILED_SEND_MAIL, email + "의 메일 전송에 실패하였습니다: " + e.getMessage()); + throw new CustomException(ErrorCode.FAILED_SEND_MAIL); } } diff --git a/src/main/java/com/gdsc_knu/official_homepage/service/admin/AdminApplicationService.java b/src/main/java/com/gdsc_knu/official_homepage/service/admin/AdminApplicationService.java index 857131c8..7ae392ea 100644 --- a/src/main/java/com/gdsc_knu/official_homepage/service/admin/AdminApplicationService.java +++ b/src/main/java/com/gdsc_knu/official_homepage/service/admin/AdminApplicationService.java @@ -15,10 +15,10 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; -import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; + import java.util.Arrays; import java.util.List; import java.util.Map; @@ -29,7 +29,9 @@ public class AdminApplicationService { private final ApplicationRepository applicationRepository; private final MailService mailService; - private final PlatformTransactionManager transactionManager; + private final TransactionTemplate transactionTemplate; + + @Transactional(readOnly = true) public AdminApplicationResponse.Statistics getStatistic() { @@ -50,7 +52,7 @@ public Map getTrackStatistic() { } private void addDefaultTrack(Map trackCountMap){ - Arrays.stream(Track.values()) + Arrays.stream(Track.getValidTrack()) .forEach(track -> trackCountMap.putIfAbsent(track.name(), 0)); } @@ -78,26 +80,19 @@ public void markApplication(Long id) { } - public void decideApplication(Long id, ApplicationStatus status) { + /** + * 메일 전송에 실패해도 status 롤백하지 않는다. + * 관리자 기능이므로 비동기로 처리하지 않고, 실행결과를 알려준다. + */ + public void decideApplication(Long id, ApplicationStatus applicationStatus) { Application application = applicationRepository.findById(id) .orElseThrow(() -> new CustomException(ErrorCode.APPLICATION_NOT_FOUND)); - updateApplicationStatus(application, status); + transactionTemplate.executeWithoutResult( + status -> application.updateStatus(applicationStatus) + ); mailService.sendEach(application); } - private void updateApplicationStatus(Application application, ApplicationStatus status) { - TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); - transactionTemplate.executeWithoutResult(transactionStatus -> { - if (status == ApplicationStatus.APPROVED) { - application.approve(); - } else if (status == ApplicationStatus.REJECTED) { - application.reject(); - } else { - throw new CustomException(ErrorCode.INVALID_APPLICATION_STATE); - } - }); - } - @Transactional public AdminApplicationResponse.Detail getApplicationDetail(Long id) { diff --git a/src/test/java/com/gdsc_knu/official_homepage/repository/application/AdminApplicationRepositoryTest.java b/src/test/java/com/gdsc_knu/official_homepage/repository/application/AdminApplicationRepositoryTest.java new file mode 100644 index 00000000..a9b70851 --- /dev/null +++ b/src/test/java/com/gdsc_knu/official_homepage/repository/application/AdminApplicationRepositoryTest.java @@ -0,0 +1,79 @@ +package com.gdsc_knu.official_homepage.repository.application; + +import com.gdsc_knu.official_homepage.OfficialHomepageApplication; +import com.gdsc_knu.official_homepage.config.QueryDslConfig; +import com.gdsc_knu.official_homepage.dto.admin.application.ApplicationStatisticType; +import com.gdsc_knu.official_homepage.entity.application.Application; +import com.gdsc_knu.official_homepage.entity.enumeration.ApplicationStatus; +import com.gdsc_knu.official_homepage.repository.ApplicationRepository; +import org.junit.jupiter.api.AfterEach; +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.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ContextConfiguration; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@Import(QueryDslConfig.class) +@ContextConfiguration(classes = OfficialHomepageApplication.class) +public class AdminApplicationRepositoryTest { + @Autowired private ApplicationRepository applicationRepository; + + @AfterEach + void clear() { + applicationRepository.deleteAll(); + } + + + @Test + @DisplayName("지원 현황을 정상적으로 카운트한다.") + void getStatistic() { + // given + int start = 1; + int countPerStatus = 5; + List temporal = createApplicationList(start, start+countPerStatus, ApplicationStatus.TEMPORAL); + start+=countPerStatus; + List save = createApplicationList(start, start+countPerStatus, ApplicationStatus.SAVED); + start+=countPerStatus; + List approve = createApplicationList(start, start+countPerStatus, ApplicationStatus.APPROVED); + start+=countPerStatus; + List reject = createApplicationList(start, start+countPerStatus, ApplicationStatus.REJECTED); + List allApplications = Stream.of(temporal, save, approve, reject) + .flatMap(List::stream) + .toList(); + applicationRepository.saveAll(allApplications); + + // when + ApplicationStatisticType statistic = applicationRepository.getStatistics(); + + // then + assertThat(statistic.getTotal()).isEqualTo(15); + assertThat(statistic.getApprovedCount()).isEqualTo(5); + assertThat(statistic.getRejectedCount()).isEqualTo(5); + assertThat(statistic.getOpenCount()).isEqualTo(0); + } + + private List createApplicationList(int startNum, int count, ApplicationStatus status){ + List applicationList = new ArrayList<>(); + for (int i=startNum; i commentList = commentRepository.findCommentAndReply(PageRequest.of(0,6), post.getId()); + + // then + assertThat(commentList).extracting("content") + .containsExactly("1","3","2","4","6","5"); + } + + + @Test + @DisplayName("부모 댓글 생성 시 자신을 그룹으로 한다.") + void save() { + // given + Comment parent = Comment.from("댓글내용",author,post,null); + Comment child = Comment.from("댓글내용",author,post,parent); + + // when + commentRepository.saveAll(List.of(parent, child)); + + // then + assertThat(parent.getParent()).isEqualTo(parent); + assertThat(child.getParent()).isEqualTo(parent); + } + + +} diff --git a/src/test/java/com/gdsc_knu/official_homepage/service/admin/AdminApplicationTest.java b/src/test/java/com/gdsc_knu/official_homepage/service/admin/AdminApplicationTest.java new file mode 100644 index 00000000..aaaa3205 --- /dev/null +++ b/src/test/java/com/gdsc_knu/official_homepage/service/admin/AdminApplicationTest.java @@ -0,0 +1,117 @@ +package com.gdsc_knu.official_homepage.service.admin; + +import com.gdsc_knu.official_homepage.entity.application.Application; +import com.gdsc_knu.official_homepage.entity.enumeration.ApplicationStatus; +import com.gdsc_knu.official_homepage.entity.enumeration.Track; +import com.gdsc_knu.official_homepage.exception.CustomException; +import com.gdsc_knu.official_homepage.repository.ApplicationRepository; +import com.gdsc_knu.official_homepage.service.MailService; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doThrow; + + +@SpringBootTest +public class AdminApplicationTest { + @Autowired private AdminApplicationService applicationService; + @Autowired private ApplicationRepository applicationRepository; + @MockBean private MailService mailService; + + + @AfterEach + void clear() { + applicationRepository.deleteAll(); + } + + @Test + @DisplayName("메일 전송에 실패하더라도 status 변경은 저장한다.") + void updateStatus() { + + } + + @Test + @DisplayName("임시저장 상태인 경우 status 를 변경할 수 없다.") + void failedUpdateTemporal() { + // given + Application application = createApplication(ApplicationStatus.TEMPORAL); + applicationRepository.save(application); + doThrow(CustomException.class).when(mailService).sendEach(application); + // when + applicationService.decideApplication(application.getId(), ApplicationStatus.APPROVED); + // then + assertThat(application.getApplicationStatus()).isEqualTo(ApplicationStatus.TEMPORAL); + } + + + @Test + @DisplayName("트랙별 지원서의 개수를 정상적으로 카운트한다.") + void getTrackStatistic() { + // given + int start = 1; + int countPerStatus = 2; + ApplicationStatus status = ApplicationStatus.SAVED; + List ai = createApplicationList(start, start+countPerStatus, Track.AI, status); + start+=countPerStatus; + List backend = createApplicationList(start, start+countPerStatus, Track.BACK_END, status); + start+=countPerStatus; + List frontend = createApplicationList(start, start+countPerStatus, Track.FRONT_END, status); + start+=countPerStatus; + List temporal = createApplicationList(start, start+countPerStatus, Track.BACK_END, ApplicationStatus.TEMPORAL); + List allApplications = Stream.of(temporal, ai, backend, frontend) + .flatMap(List::stream) + .toList(); + applicationRepository.saveAll(allApplications); + // when + Map statistic = applicationService.getTrackStatistic(); + // then + assertThat(statistic.get("BACK_END")).isEqualTo(2); + assertThat(statistic.get("FRONT_END")).isEqualTo(2); + assertThat(statistic.get("AI")).isEqualTo(2); + assertThat(statistic.get("DESIGNER")).isEqualTo(0); + assertThat(statistic.get("TOTAL")).isEqualTo(6); + assertThat(statistic.size()).isEqualTo(6); + } + + + + + + private List createApplicationList(int startNum, int count, Track track, ApplicationStatus status){ + List applicationList = new ArrayList<>(); + for (int i=startNum; i author = createAuthor(memberId); + private final Optional post = createPost(memberId); @Test @DisplayName("댓글이 정상적으로 저장된다") void saveComment() { // given - when(postRepository.findById(postId)).thenReturn(createPost()); - when(memberRepository.findById(memberId)).thenReturn(createAuthor(memberId)); + when(postRepository.findById(postId)).thenReturn(post); + when(memberRepository.findById(memberId)).thenReturn(author); // when commentService.createComment(memberId, postId, new CommentRequest.Create(null, "댓글 내용")); // then @@ -55,35 +56,31 @@ void saveComment() { @DisplayName("존재하지 않는 댓글에 답글을 남기면 오류를 발생시킨다.") void saveCommentInvalidParent() { // given - when(postRepository.findById(postId)).thenReturn(createPost()); + when(postRepository.findById(postId)).thenReturn(createPost(memberId)); when(memberRepository.findById(memberId)).thenReturn(createAuthor(memberId)); - // when - Long fakeParentId = 1L; - Exception exception = assertThrows(CustomException.class, () -> - commentService.createComment(memberId, postId, new CommentRequest.Create(fakeParentId, "댓글 내용")) - ); - //then - assertEquals(ErrorCode.COMMENT_NOT_FOUND.getMessage(), exception.getMessage()); + Long notExistCommentId = 1L; + CommentRequest.Create request = new CommentRequest.Create(notExistCommentId, "댓글 내용"); + + // when && then + assertThatExceptionOfType(CustomException.class) + .isThrownBy(() -> + commentService.createComment(memberId, postId, request) + ).withMessage(ErrorCode.COMMENT_NOT_FOUND.getMessage()); } - //TODO: 통합테스트 - @Test - @DisplayName("댓글의 순서가 올바르게 조회된다") - void getComment() { - - } @Test @DisplayName("댓글 작성자는 본인이 작성한 댓글을 수정, 삭제할 수 있다.") void getCommentByCommentAuthor() { - // given - long commentAuthorId = memberId; - when(postRepository.findById(postId)).thenReturn(createPostWithAuthor(postAuthorId)); - Comment comment = createComment(commentAuthorId); + // given (댓글을 본인이 작성했으나, 게시글은 본인이 작성한게 아닌 경우) + long postAuthorId = 2L; + when(postRepository.findById(postId)).thenReturn(createPost(postAuthorId)); + Comment comment = createComment(memberId); when(commentRepository.findCommentAndReply(PageRequest.of(0,5), postId)) .thenReturn(new PageImpl<>(Collections.singletonList(comment))); // when - PagingResponse response = commentService.getComment(memberId, postId, PageRequest.of(0,5)); + PageRequest page = PageRequest.of(0,5); + PagingResponse response = commentService.getComment(memberId, postId, page); // then assertTrue(response.getData().get(0).isCanModify()); @@ -93,14 +90,15 @@ void getCommentByCommentAuthor() { @Test @DisplayName("게시글 작성자는 해당 게시글의 댓글을 삭제할 수 있고, 수정할 수 없다.") void getCommentByPostAuthor() { - //given - long postAuthorId = memberId; - when(postRepository.findById(postId)).thenReturn(createPostWithAuthor(postAuthorId)); + //given (본인의 게시글에 다른 사람의 댓글이 존재하는 경우) + long commentAuthorId = 3L; + when(postRepository.findById(postId)).thenReturn(createPost(memberId)); Comment comment = createComment(commentAuthorId); when(commentRepository.findCommentAndReply(PageRequest.of(0,5), postId)) .thenReturn(new PageImpl<>(Collections.singletonList(comment))); // when - PagingResponse response = commentService.getComment(memberId, postId, PageRequest.of(0,5)); + PageRequest page = PageRequest.of(0,5); + PagingResponse response = commentService.getComment(memberId, postId, page); // then assertFalse(response.getData().get(0).isCanModify()); @@ -113,37 +111,27 @@ void getCommentByPostAuthor() { - public Optional createPost() { + private Optional createPost(long authorId) { return Optional.ofNullable(Post.builder() .id(postId) + .member(createAuthor(authorId).get()) .build()); } - public Optional createPostWithAuthor(long authorId) { - Member member = createAuthor(authorId).get(); - return Optional.ofNullable(Post.builder() - .id(postId) - .member(member) - .build()); - } - - public Comment createComment(long authorId) { + private Comment createComment(long authorId) { Member author = createAuthor(authorId).get(); + Comment parent = Comment.builder() + .id(1L) + .build(); return Comment.builder() .id(postId) .content("댓글") .author(author) - .parent(fakeParentComment()) - .build(); - } - - public Comment fakeParentComment() { - return Comment.builder() - .id(1L) + .parent(parent) .build(); } - public Optional createAuthor(long id) { + private Optional createAuthor(long id) { return Optional.ofNullable(Member.builder() .id(id) .email("email@email.com")