diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index cdf3bbe9..dea529ba 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -59,15 +59,15 @@ jobs: if [[ "$ENV_COLOR" == "blue" || "$ENV_COLOR" == "none" ]]; then # 블루 서버가 실행 중이거나 아무 서버도 돌아가고 있지 않다면 그린 서버를 실행 sudo docker-compose -f docker-compose.yml up -d --no-deps web-green - sleep 45 + sleep 75 # 그린 서버가 정상적으로 작동하는지 확인 (예: HTTP 상태 코드 200 확인) if curl --silent --fail http://localhost:8081; then echo "Green server is working correctly." - # 기존 서버 종료 - sudo docker-compose -f docker-compose.yml stop web-blue # Nginx를 Green 서버로 전환 sudo sed -i 's/8080/8081/' /etc/nginx/sites-available/teamspot.site sudo systemctl restart nginx + # 기존 서버 종료 + sudo docker-compose -f docker-compose.yml stop web-blue else echo "Green server is not responding correctly. Exiting." exit 1 @@ -75,15 +75,15 @@ jobs: elif [[ "$ENV_COLOR" == "green" ]]; then # 그린 서버가 실행 중이면 블루 서버를 실행 sudo docker-compose -f docker-compose.yml up -d --no-deps web-blue - sleep 45 + sleep 75 # 블루 서버가 정상적으로 작동하는지 확인 if curl --silent --fail http://localhost:8080; then echo "Blue server is working correctly." - # 기존 서버 종료 - sudo docker-compose -f docker-compose.yml stop web-green # Nginx를 Blue 서버로 전환 sudo sed -i 's/8081/8080/' /etc/nginx/sites-available/teamspot.site sudo systemctl restart nginx + # 기존 서버 종료 + sudo docker-compose -f docker-compose.yml stop web-green else echo "Blue server is not responding correctly. Exiting." exit 1 diff --git a/src/main/java/com/example/spot/domain/mapping/MemberAttendance.java b/src/main/java/com/example/spot/domain/mapping/MemberAttendance.java index 65ac46d1..a2872bf8 100644 --- a/src/main/java/com/example/spot/domain/mapping/MemberAttendance.java +++ b/src/main/java/com/example/spot/domain/mapping/MemberAttendance.java @@ -4,10 +4,7 @@ import com.example.spot.domain.Quiz; import com.example.spot.domain.common.BaseEntity; import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; @Getter @Entity @@ -36,6 +33,7 @@ public class MemberAttendance extends BaseEntity { /* ----------------------------- 생성자 ------------------------------------- */ + @Builder public MemberAttendance(Boolean isCorrect) { this.isCorrect = isCorrect; } diff --git a/src/main/java/com/example/spot/service/memberstudy/MemberStudyCommandServiceImpl.java b/src/main/java/com/example/spot/service/memberstudy/MemberStudyCommandServiceImpl.java index 0aa8fdc5..8cc85612 100644 --- a/src/main/java/com/example/spot/service/memberstudy/MemberStudyCommandServiceImpl.java +++ b/src/main/java/com/example/spot/service/memberstudy/MemberStudyCommandServiceImpl.java @@ -390,7 +390,6 @@ private static void checkStartAndFinishDate(ScheduleRequestDTO.ScheduleDTO sched /** * 출석 퀴즈를 생성하는 메서드입니다. - * * @param studyId 타겟 스터디의 아이디를 입력 받습니다. * @param scheduleId 타겟 일정의 아이디를 입력 받습니다. * @param quizRequestDTO 출석 퀴즈에 담길 질문과 정답을 입력 받습니다. diff --git a/src/main/java/com/example/spot/service/memberstudy/MemberStudyQueryServiceImpl.java b/src/main/java/com/example/spot/service/memberstudy/MemberStudyQueryServiceImpl.java index ae43ab16..3fb768dc 100644 --- a/src/main/java/com/example/spot/service/memberstudy/MemberStudyQueryServiceImpl.java +++ b/src/main/java/com/example/spot/service/memberstudy/MemberStudyQueryServiceImpl.java @@ -238,7 +238,6 @@ public StudyApplicantDTO isApplied(Long studyId) { /** * 금일 모든 스터디 회원의 출석 정보를 불러옵니다. - * * @param studyId 출석 정보를 불러올 스터디의 아이디를 입력 받습니다. * @param scheduleId 스터디 일정의 아이디를 입력 받습니다. * @param date diff --git a/src/main/java/com/example/spot/web/controller/PostController.java b/src/main/java/com/example/spot/web/controller/PostController.java index b114f50e..0785fd78 100644 --- a/src/main/java/com/example/spot/web/controller/PostController.java +++ b/src/main/java/com/example/spot/web/controller/PostController.java @@ -2,6 +2,7 @@ import com.example.spot.api.ApiResponse; import com.example.spot.api.code.status.SuccessStatus; +import com.example.spot.security.utils.SecurityUtils; import com.example.spot.service.post.PostCommandService; import com.example.spot.service.post.PostQueryService; import com.example.spot.validation.annotation.ExistMember; @@ -42,12 +43,11 @@ public class PostController { """, security = @SecurityRequirement(name = "accessToken") ) - @PostMapping(value = "/{memberId}") + @PostMapping public ApiResponse create( - @PathVariable @ExistMember Long memberId, @RequestBody @Valid PostCreateRequest postCreateRequest ) { - PostCreateResponse response = postCommandService.createPost(memberId, postCreateRequest); + PostCreateResponse response = postCommandService.createPost(SecurityUtils.getCurrentUserId(), postCreateRequest); return ApiResponse.onSuccess(SuccessStatus._CREATED, response); } @@ -139,9 +139,8 @@ public ApiResponse getPostAnnouncement() { description = "게시글 Id를 받아 게시글을 수정합니다.", security = @SecurityRequirement(name = "accessToken") ) - @PatchMapping("/{memberId}/{postId}") + @PatchMapping("/{postId}") public ApiResponse update( - @PathVariable @ExistMember Long memberId, @Parameter( description = "수정할 게시글의 ID입니다.", schema = @Schema(type = "integer", format = "int64") @@ -152,22 +151,21 @@ public ApiResponse update( ) @RequestBody PostUpdateRequest postUpdateRequest ) { - PostCreateResponse response = postCommandService.updatePost(memberId, postId, postUpdateRequest); + PostCreateResponse response = postCommandService.updatePost(SecurityUtils.getCurrentUserId(), postId, postUpdateRequest); return ApiResponse.onSuccess(SuccessStatus._OK, response); } @Tag(name = "게시판", description = "게시판 관련 API") @Operation(summary = "[게시판] 게시글 삭제 API", description = "게시글 Id를 받아 게시글을 삭제합니다.") - @DeleteMapping("/{memberId}/{postId}") + @DeleteMapping("/{postId}") public ApiResponse delete( - @PathVariable @ExistMember Long memberId, @Parameter( description = "삭제할 게시글의 ID입니다.", schema = @Schema(type = "integer", format = "int64") ) @PathVariable Long postId ) { - postCommandService.deletePost(memberId, postId); + postCommandService.deletePost(SecurityUtils.getCurrentUserId(), postId); return ApiResponse.onSuccess(SuccessStatus._NO_CONTENT); } @@ -175,21 +173,21 @@ public ApiResponse delete( @Tag(name = "게시글 좋아요", description = "게시글 좋아요 관련 API") //게시글 좋아요 @Operation(summary = "[게시판] 게시글 좋아요 API", description = "게시글 Id를 받아 게시글에 좋아요를 추가합니다.") - @PostMapping("/{postId}/{memberId}/like") + @PostMapping("/{postId}/like") public ApiResponse likePost( - @PathVariable @ExistPost Long postId, - @PathVariable @ExistMember Long memberId) { - PostLikeResponse response = postCommandService.likePost(postId, memberId); + @PathVariable @ExistPost Long postId + ) { + PostLikeResponse response = postCommandService.likePost(postId, SecurityUtils.getCurrentUserId()); return ApiResponse.onSuccess(SuccessStatus._OK, response); } @Tag(name = "게시글 좋아요", description = "게시글 좋아요 관련 API") @Operation(summary = "[게시판] 게시글 좋아요 취소 API", description = "게시글 Id를 받아 게시글에 좋아요를 취소합니다.") - @DeleteMapping("/{postId}/{memberId}/like") + @DeleteMapping("/{postId}/like") public ApiResponse cancelPostLike( - @PathVariable @ExistPost Long postId, - @PathVariable @ExistMember Long memberId) { - PostLikeResponse response = postCommandService.cancelPostLike(postId, memberId); + @PathVariable @ExistPost Long postId + ) { + PostLikeResponse response = postCommandService.cancelPostLike(postId, SecurityUtils.getCurrentUserId()); return ApiResponse.onSuccess(SuccessStatus._NO_CONTENT, response); } @@ -206,12 +204,11 @@ public ApiResponse cancelPostLike( 생성된 댓글의 고유 ID와 부모댓글 ID(parentCommentId가 0일 경우 null로 반환), 댓글 내용, 작성자를 반환합니다. """) - @PostMapping("/{postId}/{memberId}/comments") + @PostMapping("/{postId}/comments") public ApiResponse createComment( @PathVariable @ExistPost Long postId, - @PathVariable @ExistMember Long memberId, @RequestBody CommentCreateRequest request) { - CommentCreateResponse response = postCommandService.createComment(postId, memberId, request); + CommentCreateResponse response = postCommandService.createComment(postId, SecurityUtils.getCurrentUserId(), request); return ApiResponse.onSuccess(SuccessStatus._CREATED, response); } @@ -233,64 +230,64 @@ public ApiResponse getComment( //게시글 댓글 좋아요 @Tag(name = "게시판 - 댓글", description = "댓글 관련 API") @Operation(summary = "[게시판] 댓글 좋아요 API", description = "댓글 ID와 회원 ID를 받아 댓글에 좋아요를 추가합니다.") - @PostMapping("/comments/{commentId}/{memberId}/like") + @PostMapping("/comments/{commentId}/like") public ApiResponse likeComment( - @PathVariable Long commentId, - @PathVariable @ExistMember Long memberId) { - CommentLikeResponse response = postCommandService.likeComment(commentId, memberId); + @PathVariable Long commentId + ) { + CommentLikeResponse response = postCommandService.likeComment(commentId, SecurityUtils.getCurrentUserId()); return ApiResponse.onSuccess(SuccessStatus._OK, response); } @Tag(name = "게시판 - 댓글", description = "댓글 관련 API") @Operation(summary = "[게시판] 댓글 좋아요 취소 API", description = "댓글 ID와 회원 ID를 받아 댓글에 좋아요를 취소합니다.") - @DeleteMapping("/comments/{commentId}/{memberId}/like") + @DeleteMapping("/comments/{commentId}/like") public ApiResponse cancelCommentLike( - @PathVariable Long commentId, - @PathVariable @ExistMember Long memberId) { - CommentLikeResponse response = postCommandService.cancelCommentLike(commentId, memberId); + @PathVariable Long commentId + ) { + CommentLikeResponse response = postCommandService.cancelCommentLike(commentId, SecurityUtils.getCurrentUserId()); return ApiResponse.onSuccess(SuccessStatus._NO_CONTENT, response); } //게시글 댓글 싫어요 @Tag(name = "게시판 - 댓글", description = "댓글 관련 API") @Operation(summary = "[게시판] 댓글 싫어요 API", description = "댓글 ID와 회원 ID를 받아 댓글에 싫어요를 추가합니다.") - @PostMapping("/comments/{commentId}/{memberId}/dislike") + @PostMapping("/comments/{commentId}/dislike") public ApiResponse dislikeComment( - @PathVariable Long commentId, - @PathVariable @ExistMember Long memberId) { - CommentLikeResponse response = postCommandService.dislikeComment(commentId, memberId); + @PathVariable Long commentId + ) { + CommentLikeResponse response = postCommandService.dislikeComment(commentId, SecurityUtils.getCurrentUserId()); return ApiResponse.onSuccess(SuccessStatus._OK, response); } @Tag(name = "게시판 - 댓글", description = "댓글 관련 API") @Operation(summary = "[게시판] 댓글 싫어요 취소 API", description = "댓글 ID와 회원 ID를 받아 댓글에 싫어요를 취소합니다.") - @DeleteMapping("/comments/{commentId}/{memberId}/dislike") + @DeleteMapping("/comments/{commentId}/dislike") public ApiResponse cancelCommentDislike( - @PathVariable Long commentId, - @PathVariable @ExistMember Long memberId) { - CommentLikeResponse response = postCommandService.cancelCommentDislike(commentId, memberId); + @PathVariable Long commentId + ) { + CommentLikeResponse response = postCommandService.cancelCommentDislike(commentId, SecurityUtils.getCurrentUserId()); return ApiResponse.onSuccess(SuccessStatus._NO_CONTENT, response); } //스크랩 @Tag(name = "게시글 스크랩", description = "게시글 스크랩 관련 API") @Operation(summary = "[게시판] 게시글 스크랩 API", description = "게시글 ID와 회원 ID를 받아 스크랩을 추가합니다.") - @PostMapping("/{postId}/{memberId}/scrap") + @PostMapping("/{postId}/scrap") public ApiResponse scrapPost( - @PathVariable @ExistPost Long postId, - @PathVariable @ExistMember Long memberId) { - ScrapPostResponse response = postCommandService.scrapPost(postId, memberId); + @PathVariable @ExistPost Long postId + ) { + ScrapPostResponse response = postCommandService.scrapPost(postId, SecurityUtils.getCurrentUserId()); return ApiResponse.onSuccess(SuccessStatus._OK, response); } @Tag(name = "게시글 스크랩", description = "게시글 스크랩 관련 API") @Operation(summary = "[게시판] 게시글 스크랩 취소 API", description = "게시글 ID와 회원 ID를 받아 스크랩을 취소합니다.") - @DeleteMapping("/{postId}/{memberId}/scrap") + @DeleteMapping("/{postId}/scrap") public ApiResponse cancelPostScrap( - @PathVariable @ExistPost Long postId, - @PathVariable @ExistMember Long memberId) { - ScrapPostResponse response = postCommandService.cancelPostScrap(postId, memberId); + @PathVariable @ExistPost Long postId + ) { + ScrapPostResponse response = postCommandService.cancelPostScrap(postId, SecurityUtils.getCurrentUserId()); return ApiResponse.onSuccess(SuccessStatus._NO_CONTENT, response); } diff --git a/src/main/java/com/example/spot/web/controller/SearchController.java b/src/main/java/com/example/spot/web/controller/SearchController.java index bf1e82ad..60824af8 100644 --- a/src/main/java/com/example/spot/web/controller/SearchController.java +++ b/src/main/java/com/example/spot/web/controller/SearchController.java @@ -44,7 +44,6 @@ public class SearchController { ## [메인 화면] 접속한 회원의 추천 스터디 3개를 조회 합니다. 조회된 스터디 3개의 정보가 반환 됩니다.""", security = @SecurityRequirement(name = "accessToken")) - @Parameter(name = "memberId", description = "조회할 유저의 ID를 입력 받습니다.", required = true) public ApiResponse recommendStudiesForMain() { StudyPreviewDTO recommendStudies = studyQueryService.findRecommendStudies(SecurityUtils.getCurrentUserId()); return ApiResponse.onSuccess(SuccessStatus._STUDY_FOUND, recommendStudies); @@ -68,7 +67,6 @@ public ApiResponse interestedStudiesForMain() { ## [마이 페이지] 마이 페이지에 들어갈 나와 관련된 스터디 갯수 정보를 조회합니다. 스터디 갯수 정보와 내 이름이 반환 됩니다.""", security = @SecurityRequirement(name = "accessToken")) - @Parameter(name = "memberId", description = "조회할 유저의 ID를 입력 받습니다.", required = true) public ApiResponse myPage() { MyPageDTO myPageStudyCount = studyQueryService.getMyPageStudyCount(SecurityUtils.getCurrentUserId()); return ApiResponse.onSuccess(SuccessStatus._STUDY_FOUND, myPageStudyCount); diff --git a/src/main/java/com/example/spot/web/dto/memberstudy/request/StudyQuizRequestDTO.java b/src/main/java/com/example/spot/web/dto/memberstudy/request/StudyQuizRequestDTO.java index 4f3a13db..c28727ce 100644 --- a/src/main/java/com/example/spot/web/dto/memberstudy/request/StudyQuizRequestDTO.java +++ b/src/main/java/com/example/spot/web/dto/memberstudy/request/StudyQuizRequestDTO.java @@ -3,6 +3,7 @@ import com.example.spot.validation.annotation.ExistMember; import com.example.spot.validation.annotation.TextLength; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -12,6 +13,7 @@ public class StudyQuizRequestDTO { @Getter + @Builder @NoArgsConstructor @AllArgsConstructor public static class QuizDTO { @@ -26,6 +28,7 @@ public static class QuizDTO { } @Getter + @Builder @NoArgsConstructor @AllArgsConstructor public static class AttendanceDTO { diff --git a/src/test/java/com/example/spot/service/studyattendance/StudyAttendanceCommandServiceTest.java b/src/test/java/com/example/spot/service/studyattendance/StudyAttendanceCommandServiceTest.java new file mode 100644 index 00000000..b4a7a33d --- /dev/null +++ b/src/test/java/com/example/spot/service/studyattendance/StudyAttendanceCommandServiceTest.java @@ -0,0 +1,670 @@ +package com.example.spot.service.studyattendance; + +import com.example.spot.api.exception.handler.StudyHandler; +import com.example.spot.domain.Member; +import com.example.spot.domain.Quiz; +import com.example.spot.domain.enums.ApplicationStatus; +import com.example.spot.domain.enums.Gender; +import com.example.spot.domain.mapping.MemberAttendance; +import com.example.spot.domain.mapping.MemberStudy; +import com.example.spot.domain.study.Schedule; +import com.example.spot.domain.study.Study; +import com.example.spot.repository.*; +import com.example.spot.service.memberstudy.MemberStudyCommandServiceImpl; +import com.example.spot.web.dto.memberstudy.request.StudyQuizRequestDTO; +import com.example.spot.web.dto.memberstudy.response.StudyQuizResponseDTO; +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 org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class StudyAttendanceCommandServiceTest { + + @Mock + private MemberRepository memberRepository; + + @Mock + private StudyRepository studyRepository; + @Mock + private MemberStudyRepository memberStudyRepository; + + @Mock + private ScheduleRepository scheduleRepository; + @Mock + private QuizRepository quizRepository; + @Mock + private MemberAttendanceRepository memberAttendanceRepository; + + @InjectMocks + private MemberStudyCommandServiceImpl memberStudyCommandService; + + private static Study study; + private static Member member1; + private static Member member2; + private static Member owner; + private static MemberStudy member1Study; + private static MemberStudy ownerStudy; + private static Schedule schedule; + private static Quiz quiz1; + private static MemberAttendance member1Attendance; + private static MemberAttendance member1Attendance2; + private static MemberAttendance member1Attendance3; + private static MemberAttendance ownerAttendance; + + @Mock + private static Quiz quiz2; + + @Mock + private static MemberAttendance mockAttendance; + + private static final LocalDate date = LocalDate.now(); + private static final LocalDateTime now = LocalDateTime.now(); + + @BeforeEach + void setUp() { + initMember(); + initStudy(); + initMemberStudy(); + initSchedule(); + + when(memberRepository.findById(member1.getId())).thenReturn(Optional.of(member1)); + when(memberRepository.findById(member2.getId())).thenReturn(Optional.of(member2)); + when(memberRepository.findById(owner.getId())).thenReturn(Optional.of(owner)); + + when(studyRepository.findById(1L)).thenReturn(Optional.of(study)); + + when(memberStudyRepository.findByMemberIdAndStudyIdAndIsOwned(owner.getId(), 1L, Boolean.TRUE)) + .thenReturn(Optional.of(ownerStudy)); + when(memberStudyRepository.findByMemberIdAndStudyIdAndStatus(member1.getId(), 1L, ApplicationStatus.APPROVED)) + .thenReturn(Optional.of(member1Study)); + when(memberStudyRepository.findByMemberIdAndStudyIdAndStatus(owner.getId(), 1L, ApplicationStatus.APPROVED)) + .thenReturn(Optional.of(ownerStudy)); + when(memberStudyRepository.findAllByStudyIdAndStatus(1L, ApplicationStatus.APPROVED)) + .thenReturn(List.of(member1Study, ownerStudy)); + + when(scheduleRepository.findById(schedule.getId())).thenReturn(Optional.of(schedule)); + } + + @Test + @DisplayName("스터디 퀴즈 생성 - (성공)") + void createAttendanceQuiz_Success() { + + // given + Long studyId = 1L; + + // 사용자 인증 정보 생성 + StudyQuizRequestDTO.QuizDTO quizRequestDTO = getQuizDTO(owner); + + LocalDateTime startOfDay = quizRequestDTO.getCreatedAt().withHour(0).withMinute(0).withSecond(0).withNano(0); + LocalDateTime endOfDay = quizRequestDTO.getCreatedAt().withHour(23).withMinute(59).withSecond(59).withNano(999_999_000); + + when(quizRepository.findAllByScheduleIdAndCreatedAtBetween(schedule.getId(), startOfDay, endOfDay)) + .thenReturn(List.of()); + when(quizRepository.save(any(Quiz.class))).thenReturn(quiz2); + + // when + StudyQuizResponseDTO.QuizDTO result = memberStudyCommandService.createAttendanceQuiz(studyId, schedule.getId(), quizRequestDTO); + + // then + assertThat(result).isNotNull(); + assertThat(result.getQuestion()).isEqualTo("최고의 스터디 앱은?"); + verify(quizRepository, times(1)).save(any(Quiz.class)); + } + + @Test + @DisplayName("스터디 퀴즈 생성 - 스터디가 존재하지 않는 경우 (실패)") + void createAttendanceQuiz_StudyNotFound_Fail() { + + // given + Long studyId = 2L; + StudyQuizRequestDTO.QuizDTO quizRequestDTO = getQuizDTO(owner); + + LocalDateTime startOfDay = quizRequestDTO.getCreatedAt().withHour(0).withMinute(0).withSecond(0).withNano(0); + LocalDateTime endOfDay = quizRequestDTO.getCreatedAt().withHour(23).withMinute(59).withSecond(59).withNano(999_999_000); + + when(quizRepository.findAllByScheduleIdAndCreatedAtBetween(schedule.getId(), startOfDay, endOfDay)) + .thenReturn(List.of()); + when(quizRepository.save(any(Quiz.class))).thenReturn(quiz2); + + // when & then + assertThrows(StudyHandler.class, () -> memberStudyCommandService.createAttendanceQuiz(studyId, schedule.getId(), quizRequestDTO)); + } + + @Test + @DisplayName("스터디 퀴즈 생성 - 스터디 회원이 아닌 경우 (실패)") + void createAttendanceQuiz_NotStudyMember_Fail() { + + // given + Long studyId = 1L; + StudyQuizRequestDTO.QuizDTO quizRequestDTO = getQuizDTO(member2); + + LocalDateTime startOfDay = quizRequestDTO.getCreatedAt().withHour(0).withMinute(0).withSecond(0).withNano(0); + LocalDateTime endOfDay = quizRequestDTO.getCreatedAt().withHour(23).withMinute(59).withSecond(59).withNano(999_999_000); + + when(quizRepository.findAllByScheduleIdAndCreatedAtBetween(schedule.getId(), startOfDay, endOfDay)) + .thenReturn(List.of()); + when(quizRepository.save(any(Quiz.class))).thenReturn(quiz2); + + // when & then + assertThrows(StudyHandler.class, () -> memberStudyCommandService.createAttendanceQuiz(studyId, schedule.getId(), quizRequestDTO)); + } + + @Test + @DisplayName("스터디 퀴즈 생성 - 스터디 관리자가 아닌 경우 (실패)") + void createAttendanceQuiz_NotOwner_Fail() { + + // given + Long studyId = 1L; + StudyQuizRequestDTO.QuizDTO quizRequestDTO = getQuizDTO(member1); + + LocalDateTime startOfDay = quizRequestDTO.getCreatedAt().withHour(0).withMinute(0).withSecond(0).withNano(0); + LocalDateTime endOfDay = quizRequestDTO.getCreatedAt().withHour(23).withMinute(59).withSecond(59).withNano(999_999_000); + + when(quizRepository.findAllByScheduleIdAndCreatedAtBetween(schedule.getId(), startOfDay, endOfDay)) + .thenReturn(List.of()); + when(quizRepository.save(any(Quiz.class))).thenReturn(quiz2); + + // when & then + assertThrows(StudyHandler.class, () -> memberStudyCommandService.createAttendanceQuiz(studyId, schedule.getId(), quizRequestDTO)); + } + + @Test + @DisplayName("스터디 퀴즈 생성 - 이미 요청한 날짜에 스터디 퀴즈가 존재하는 경우 (실패)") + void createAttendanceQuiz_ScheduleNotFound_Fail() { + + // given + initMemberAttendance(); + initQuiz(); + + Long studyId = 1L; + StudyQuizRequestDTO.QuizDTO quizRequestDTO = getQuizDTO(member1); + + LocalDateTime startOfDay = quizRequestDTO.getCreatedAt().withHour(0).withMinute(0).withSecond(0).withNano(0); + LocalDateTime endOfDay = quizRequestDTO.getCreatedAt().withHour(23).withMinute(59).withSecond(59).withNano(999_999_000); + + when(quizRepository.findAllByScheduleIdAndCreatedAtBetween(schedule.getId(), startOfDay, endOfDay)) + .thenReturn(List.of(quiz1)); + when(quizRepository.save(any(Quiz.class))).thenReturn(quiz2); + + // when & then + assertThrows(StudyHandler.class, () -> memberStudyCommandService.createAttendanceQuiz(studyId, schedule.getId(), quizRequestDTO)); + } + + @Test + @DisplayName("스터디 출석 - (성공)") + void attendantStudy_Success() { + + // given + initMemberAttendance(); + initQuiz(); + + Long studyId = 1L; + Long scheduleId = schedule.getId(); + + StudyQuizRequestDTO.AttendanceDTO attendanceRequestDTO = getAttendanceDTO(member1, now); + + LocalDateTime startOfDay = attendanceRequestDTO.getDateTime().withHour(0).withMinute(0).withSecond(0).withNano(0); + LocalDateTime endOfDay = attendanceRequestDTO.getDateTime().withHour(23).withMinute(59).withSecond(59).withNano(999_999_000); + + when(quizRepository.findAllByScheduleIdAndCreatedAtBetween(schedule.getId(), startOfDay, endOfDay)) + .thenReturn(List.of(quiz1)); + when(memberAttendanceRepository.save(any(MemberAttendance.class))).thenReturn(mockAttendance); + when(memberStudyRepository.findByMemberIdAndStudyIdAndStatus(member1.getId(), null, ApplicationStatus.APPROVED)) + .thenReturn(Optional.of(member1Study)); + when(memberAttendanceRepository.findByQuizIdAndMemberId(null, member1.getId())) + .thenReturn(List.of(member1Attendance)); + + // when + StudyQuizResponseDTO.AttendanceDTO result = memberStudyCommandService.attendantStudy(studyId, scheduleId, attendanceRequestDTO); + + // then + assertThat(result).isNotNull(); + assertThat(result.getMemberId()).isEqualTo(member1.getId()); + assertThat(result.getTryNum()).isEqualTo(2); + assertThat(result.getIsCorrect()).isEqualTo(true); + + verify(memberAttendanceRepository, times(1)).save(any(MemberAttendance.class)); + } + + @Test + @DisplayName("스터디 출석 - 스터디 회원이 아닌 경우 (실패)") + void attendantStudy_NotStudyMember_Fail() { + + // given + initMemberAttendance(); + initQuiz(); + + Long studyId = 1L; + Long scheduleId = schedule.getId(); + + StudyQuizRequestDTO.AttendanceDTO attendanceRequestDTO = getAttendanceDTO(); + + LocalDateTime startOfDay = attendanceRequestDTO.getDateTime().withHour(0).withMinute(0).withSecond(0).withNano(0); + LocalDateTime endOfDay = attendanceRequestDTO.getDateTime().withHour(23).withMinute(59).withSecond(59).withNano(999_999_000); + + when(quizRepository.findAllByScheduleIdAndCreatedAtBetween(schedule.getId(), startOfDay, endOfDay)) + .thenReturn(List.of(quiz1)); + when(memberAttendanceRepository.save(any(MemberAttendance.class))).thenReturn(mockAttendance); + when(memberAttendanceRepository.findByQuizIdAndMemberId(null, member2.getId())) + .thenReturn(List.of()); + + // when & then + assertThrows(StudyHandler.class, () -> memberStudyCommandService.attendantStudy(studyId, scheduleId, attendanceRequestDTO)); + } + + @Test + @DisplayName("스터디 출석 - 요청 날짜에 출석 퀴즈가 존재하지 않는 경우 (실패)") + void attendantStudy_QuizNotFound_Fail() { + + // given + initMemberAttendance(); + initQuiz(); + + Long studyId = 1L; + Long scheduleId = 2L; + + // 사용자 인증 정보 생성 + getAuthentication(member1.getId()); + + StudyQuizRequestDTO.AttendanceDTO attendanceRequestDTO = getAttendanceDTO(); + + LocalDateTime startOfDay = attendanceRequestDTO.getDateTime().withHour(0).withMinute(0).withSecond(0).withNano(0); + LocalDateTime endOfDay = attendanceRequestDTO.getDateTime().withHour(23).withMinute(59).withSecond(59).withNano(999_999_000); + + when(quizRepository.findAllByScheduleIdAndCreatedAtBetween(schedule.getId(), startOfDay, endOfDay)) + .thenReturn(List.of(quiz1)); + when(memberAttendanceRepository.save(any(MemberAttendance.class))).thenReturn(mockAttendance); + when(quizRepository.findAllByScheduleIdAndCreatedAtBetween(scheduleId, startOfDay, endOfDay)) + .thenReturn(List.of()); + when(memberAttendanceRepository.findByQuizIdAndMemberId(null, member1.getId())) + .thenReturn(List.of(member1Attendance)); + + // when & then + assertThrows(StudyHandler.class, () -> memberStudyCommandService.attendantStudy(studyId, scheduleId, attendanceRequestDTO)); + } + + @Test + @DisplayName("스터디 출석 - 퀴즈 제한시간이 끝난 경우 (실패)") + void attendantStudy_QuizTimeOver_Fail() { + + // given + initMemberAttendance(); + initQuiz(); + + Long studyId = 1L; + Long scheduleId = schedule.getId(); + + // 사용자 인증 정보 생성 + getAuthentication(member1.getId()); + + StudyQuizRequestDTO.AttendanceDTO attendanceRequestDTO = StudyQuizRequestDTO.AttendanceDTO.builder() + .dateTime(now.plusMinutes(5).plusNanos(1)) + .answer("SPOT") + .build(); + + LocalDateTime startOfDay = attendanceRequestDTO.getDateTime().withHour(0).withMinute(0).withSecond(0).withNano(0); + LocalDateTime endOfDay = attendanceRequestDTO.getDateTime().withHour(23).withMinute(59).withSecond(59).withNano(999_999_000); + + when(quizRepository.findAllByScheduleIdAndCreatedAtBetween(schedule.getId(), startOfDay, endOfDay)) + .thenReturn(List.of(quiz1)); + when(memberAttendanceRepository.save(any(MemberAttendance.class))).thenReturn(mockAttendance); + when(memberStudyRepository.findByMemberIdAndStudyIdAndStatus(member1.getId(), null, ApplicationStatus.APPROVED)) + .thenReturn(Optional.of(member1Study)); + when(memberAttendanceRepository.findByQuizIdAndMemberId(null, member1.getId())) + .thenReturn(List.of()); + + // when & then + assertThrows(StudyHandler.class, () -> memberStudyCommandService.attendantStudy(studyId, scheduleId, attendanceRequestDTO)); + } + + @Test + @DisplayName("스터디 출석 - 퀴즈 시도 횟수를 초과한 경우 (실패)") + void attendantStudy_QuizTryOver_Fail() { + + // given + initMemberAttendance(); + initQuiz(); + + Long studyId = 1L; + Long scheduleId = schedule.getId(); + + // 사용자 인증 정보 생성 + StudyQuizRequestDTO.AttendanceDTO attendanceRequestDTO = getAttendanceDTO(member1, now.plusMinutes(5).plusNanos(1)); + + LocalDateTime startOfDay = attendanceRequestDTO.getDateTime().withHour(0).withMinute(0).withSecond(0).withNano(0); + LocalDateTime endOfDay = attendanceRequestDTO.getDateTime().withHour(23).withMinute(59).withSecond(59).withNano(999_999_000); + + when(quizRepository.findAllByScheduleIdAndCreatedAtBetween(schedule.getId(), startOfDay, endOfDay)) + .thenReturn(List.of(quiz1)); + when(memberAttendanceRepository.save(any(MemberAttendance.class))).thenReturn(mockAttendance); + when(memberStudyRepository.findByMemberIdAndStudyIdAndStatus(member1.getId(), null, ApplicationStatus.APPROVED)) + .thenReturn(Optional.of(member1Study)); + when(memberAttendanceRepository.findByQuizIdAndMemberId(null, member1.getId())) + .thenReturn(List.of(member1Attendance, member1Attendance2, member1Attendance3)); + + // when & then + assertThrows(StudyHandler.class, () -> memberStudyCommandService.attendantStudy(studyId, scheduleId, attendanceRequestDTO)); + } + + @Test + @DisplayName("스터디 출석 - 이미 출석이 완료된 경우 (실패)") + void attendantStudy_QuizAlreadyCorrect_Fail() { + + // given + initMemberAttendance(); + initQuiz(); + + Long studyId = 1L; + Long scheduleId = schedule.getId(); + + StudyQuizRequestDTO.AttendanceDTO attendanceRequestDTO = getAttendanceDTO(owner, now.plusMinutes(5).plusNanos(1)); + + LocalDateTime startOfDay = attendanceRequestDTO.getDateTime().withHour(0).withMinute(0).withSecond(0).withNano(0); + LocalDateTime endOfDay = attendanceRequestDTO.getDateTime().withHour(23).withMinute(59).withSecond(59).withNano(999_999_000); + + when(quizRepository.findAllByScheduleIdAndCreatedAtBetween(schedule.getId(), startOfDay, endOfDay)) + .thenReturn(List.of(quiz1)); + when(memberAttendanceRepository.save(any(MemberAttendance.class))).thenReturn(mockAttendance); + when(memberStudyRepository.findByMemberIdAndStudyIdAndStatus(owner.getId(), null, ApplicationStatus.APPROVED)) + .thenReturn(Optional.of(ownerStudy)); + when(memberAttendanceRepository.findByQuizIdAndMemberId(null, owner.getId())) + .thenReturn(List.of(ownerAttendance)); + + // when & then + assertThrows(StudyHandler.class, () -> memberStudyCommandService.attendantStudy(studyId, scheduleId, attendanceRequestDTO)); + } + + @Test + @DisplayName("스터디 출석 퀴즈 삭제 - (성공)") + void deleteAttendanceQuiz_Success() { + + // given + initMemberAttendance(); + initQuiz(); + + Long studyId = 1L; + Long scheduleId = schedule.getId(); + + // 사용자 인증 정보 생성 + getAuthentication(owner.getId()); + + LocalDateTime startOfDay = date.atStartOfDay(); // 오늘 날짜 + LocalDateTime endOfDay = date.atStartOfDay().plusDays(1); // 내일 날짜 + + when(quizRepository.findAllByScheduleIdAndCreatedAtBetween(schedule.getId(), startOfDay, endOfDay)) + .thenReturn(List.of(quiz1)); + when(memberStudyRepository.findByMemberIdAndStudyIdAndStatus(owner.getId(), null, ApplicationStatus.APPROVED)) + .thenReturn(Optional.of(ownerStudy)); + when(memberAttendanceRepository.findByQuizId(quiz1.getId())) + .thenReturn(List.of(member1Attendance, member1Attendance2, member1Attendance3, ownerAttendance)); + + // when + StudyQuizResponseDTO.QuizDTO result = memberStudyCommandService.deleteAttendanceQuiz(studyId, scheduleId, date); + + // then + assertThat(result).isNotNull(); + assertThat(result.getQuestion()).isEqualTo("최고의 스터디 앱은?"); + assertThat(result.getCreatedAt()).isEqualTo(now); + verify(quizRepository, times(1)).delete(any(Quiz.class)); + } + + @Test + @DisplayName("스터디 출석 퀴즈 삭제 - 스터디 퀴즈가 없는 경우 (실패)") + void deleteAttendanceQuiz_QuizNotFound_Fail() { + + // given + initMemberAttendance(); + initQuiz(); + + Long studyId = 1L; + Long scheduleId = 2L; + + // 사용자 인증 정보 생성 + getAuthentication(owner.getId()); + + LocalDateTime startOfDay = date.atStartOfDay(); // 오늘 날짜 + LocalDateTime endOfDay = date.atStartOfDay().plusDays(1); // 내일 날짜 + + when(quizRepository.findAllByScheduleIdAndCreatedAtBetween(2L, startOfDay, endOfDay)) + .thenReturn(List.of()); + when(memberStudyRepository.findByMemberIdAndStudyIdAndStatus(owner.getId(), null, ApplicationStatus.APPROVED)) + .thenReturn(Optional.of(ownerStudy)); + when(memberAttendanceRepository.findByQuizId(quiz1.getId())) + .thenReturn(List.of(member1Attendance, member1Attendance2, member1Attendance3, ownerAttendance)); + + // when & then + assertThrows(StudyHandler.class, () -> memberStudyCommandService.deleteAttendanceQuiz(studyId, scheduleId, date)); + } + + @Test + @DisplayName("스터디 출석 퀴즈 삭제 - 스터디 회원이 아닌 경우 (실패)") + void deleteAttendanceQuiz_NotStudyMember_Fail() { + + // given + initMemberAttendance(); + initQuiz(); + + Long studyId = 1L; + Long scheduleId = schedule.getId(); + + // 사용자 인증 정보 생성 + getAuthentication(member2.getId()); + + LocalDateTime startOfDay = date.atStartOfDay(); // 오늘 날짜 + LocalDateTime endOfDay = date.atStartOfDay().plusDays(1); // 내일 날짜 + + when(quizRepository.findAllByScheduleIdAndCreatedAtBetween(scheduleId, startOfDay, endOfDay)) + .thenReturn(List.of(quiz1)); + when(memberAttendanceRepository.findByQuizId(quiz1.getId())) + .thenReturn(List.of(member1Attendance, member1Attendance2, member1Attendance3, ownerAttendance)); + + // when & then + assertThrows(StudyHandler.class, () -> memberStudyCommandService.deleteAttendanceQuiz(studyId, scheduleId, date)); + } + + @Test + @DisplayName("스터디 출석 퀴즈 삭제 - 스터디장이 아닌 경우 (실패)") + void deleteAttendanceQuiz_NotStudyOwner_Fail() { + + // given + initMemberAttendance(); + initQuiz(); + + Long studyId = 1L; + Long scheduleId = schedule.getId(); + + // 사용자 인증 정보 생성 + getAuthentication(member1.getId()); + + LocalDateTime startOfDay = date.atStartOfDay(); // 오늘 날짜 + LocalDateTime endOfDay = date.atStartOfDay().plusDays(1); // 내일 날짜 + + when(quizRepository.findAllByScheduleIdAndCreatedAtBetween(scheduleId, startOfDay, endOfDay)) + .thenReturn(List.of(quiz1)); + when(memberStudyRepository.findByMemberIdAndStudyIdAndStatus(owner.getId(), null, ApplicationStatus.APPROVED)) + .thenReturn(Optional.of(member1Study)); + when(memberAttendanceRepository.findByQuizId(quiz1.getId())) + .thenReturn(List.of(member1Attendance, member1Attendance2, member1Attendance3, ownerAttendance)); + + // when & then + assertThrows(StudyHandler.class, () -> memberStudyCommandService.deleteAttendanceQuiz(studyId, scheduleId, date)); + } + +/*-------------------------------------------------------- Utils ------------------------------------------------------------------------*/ + + private static void initMember() { + member1 = Member.builder() + .id(1L) + .scheduleList(new ArrayList<>()) + .build(); + member2 = Member.builder() + .id(2L) + .scheduleList(new ArrayList<>()) + .build(); + owner = Member.builder() + .id(3L) + .scheduleList(new ArrayList<>()) + .build(); + } + + private static void initStudy() { + study = Study.builder() + .gender(Gender.MALE) + .minAge(20) + .maxAge(29) + .fee(10000) + .profileImage("a.jpg") + .hasFee(true) + .isOnline(true) + .goal("SQLD") + .introduction("SQLD 자격증 스터디") + .title("SQLD Master") + .maxPeople(10L) + .build(); + } + + private static void initMemberStudy() { + ownerStudy = MemberStudy.builder() + .id(1L) + .status(ApplicationStatus.APPROVED) + .isOwned(true) + .introduction("Hi") + .member(owner) + .study(study) + .build(); + member1Study = MemberStudy.builder() + .id(2L) + .status(ApplicationStatus.APPROVED) + .isOwned(false) + .introduction("Hi") + .member(member1) + .study(study) + .build(); + } + + private static void initSchedule() { + schedule = Schedule.builder() + .id(1L) + .study(study) + .member(owner) + .build(); + study.addSchedule(schedule); + owner.addSchedule(schedule); + } + + private static void initQuiz() { + quiz1 = Quiz.builder() + .schedule(schedule) + .member(owner) + .question("최고의 스터디 앱은?") + .answer("SPOT") + .createdAt(now) + .build(); + quiz1.addMemberAttendance(member1Attendance); + quiz1.addMemberAttendance(ownerAttendance); + } + + private static void initMemberAttendance() { + member1Attendance = MemberAttendance.builder() + .isCorrect(false) + .build(); + member1Attendance.setMember(member1); + member1Attendance.setQuiz(quiz1); + + ownerAttendance = MemberAttendance.builder() + .isCorrect(true) + .build(); + ownerAttendance.setMember(owner); + ownerAttendance.setQuiz(quiz1); + + member1Attendance2 = MemberAttendance.builder() + .isCorrect(false) + .build(); + member1Attendance2.setMember(member1); + member1Attendance2.setQuiz(quiz1); + + member1Attendance3 = MemberAttendance.builder() + .isCorrect(false) + .build(); + member1Attendance3.setMember(member1); + member1Attendance3.setQuiz(quiz1); + } + + private static void getAuthentication(Long memberId) { + String idString = String.valueOf(memberId); + Authentication authentication = new UsernamePasswordAuthenticationToken(idString, null, Collections.emptyList()); + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(authentication); + SecurityContextHolder.setContext(securityContext); + } + + private StudyQuizRequestDTO.QuizDTO getQuizDTO(Member member1) { + + // 사용자 인증 정보 생성 + getAuthentication(member1.getId()); + + StudyQuizRequestDTO.QuizDTO quizRequestDTO = StudyQuizRequestDTO.QuizDTO + .builder() + .createdAt(now) + .question("question") + .answer("answer") + .build(); + + quiz2 = Quiz.builder() + .schedule(schedule) + .member(member1) + .question("최고의 스터디 앱은?") + .answer("SPOT") + .createdAt(now) + .build(); + return quizRequestDTO; + } + + private static StudyQuizRequestDTO.AttendanceDTO getAttendanceDTO() { + // 사용자 인증 정보 생성 + getAuthentication(member2.getId()); + return StudyQuizRequestDTO.AttendanceDTO.builder() + .dateTime(now) + .answer("SPOT") + .build(); + } + + private static StudyQuizRequestDTO.AttendanceDTO getAttendanceDTO(Member owner, LocalDateTime now) { + // 사용자 인증 정보 생성 + getAuthentication(owner.getId()); + + StudyQuizRequestDTO.AttendanceDTO attendanceRequestDTO = StudyQuizRequestDTO.AttendanceDTO.builder() + .dateTime(now) + .answer("SPOT") + .build(); + + mockAttendance = MemberAttendance.builder() + .isCorrect(true) + .build(); + mockAttendance.setMember(owner); + mockAttendance.setQuiz(quiz1); + return attendanceRequestDTO; + } +} \ No newline at end of file diff --git a/src/test/java/com/example/spot/service/studyattendance/StudyAttendanceQueryServiceTest.java b/src/test/java/com/example/spot/service/studyattendance/StudyAttendanceQueryServiceTest.java new file mode 100644 index 00000000..4d4341c3 --- /dev/null +++ b/src/test/java/com/example/spot/service/studyattendance/StudyAttendanceQueryServiceTest.java @@ -0,0 +1,324 @@ +package com.example.spot.service.studyattendance; + +import com.example.spot.api.exception.handler.StudyHandler; +import com.example.spot.domain.Member; +import com.example.spot.domain.Quiz; +import com.example.spot.domain.enums.ApplicationStatus; +import com.example.spot.domain.enums.Gender; +import com.example.spot.domain.mapping.MemberAttendance; +import com.example.spot.domain.mapping.MemberStudy; +import com.example.spot.domain.study.Schedule; +import com.example.spot.domain.study.Study; +import com.example.spot.repository.*; +import com.example.spot.service.memberstudy.MemberStudyQueryServiceImpl; +import com.example.spot.web.dto.memberstudy.response.StudyQuizResponseDTO; +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 org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class StudyAttendanceQueryServiceTest { + + @Mock + private MemberRepository memberRepository; + + @Mock + private StudyRepository studyRepository; + @Mock + private MemberStudyRepository memberStudyRepository; + + @Mock + private ScheduleRepository scheduleRepository; + @Mock + private QuizRepository quizRepository; + @Mock + private MemberAttendanceRepository memberAttendanceRepository; + + @InjectMocks + private MemberStudyQueryServiceImpl memberStudyQueryService; + + private static Study study; + private static Member member1; + private static Member member2; + private static Member owner; + private static MemberStudy member1Study; + private static MemberStudy ownerStudy; + private static Schedule schedule; + private static Quiz quiz; + private static MemberAttendance member1Attendance; + private static MemberAttendance ownerAttendance; + + private static final LocalDate date = LocalDate.now(); + + @BeforeEach + void setUp() { + initMember(); + initStudy(); + initMemberStudy(); + initSchedule(); + initMemberAttendance(); + initQuiz(); + + when(memberRepository.findById(member1.getId())).thenReturn(Optional.of(member1)); + when(memberRepository.findById(member2.getId())).thenReturn(Optional.of(member2)); + when(memberRepository.findById(owner.getId())).thenReturn(Optional.of(owner)); + + when(studyRepository.findById(1L)).thenReturn(Optional.of(study)); + + when(memberStudyRepository.findByMemberIdAndStudyIdAndStatus(member1.getId(), 1L, ApplicationStatus.APPROVED)) + .thenReturn(Optional.of(member1Study)); + when(memberStudyRepository.findByMemberIdAndStudyIdAndStatus(owner.getId(), 1L, ApplicationStatus.APPROVED)) + .thenReturn(Optional.of(ownerStudy)); + when(memberStudyRepository.findAllByStudyIdAndStatus(1L, ApplicationStatus.APPROVED)) + .thenReturn(List.of(member1Study, ownerStudy)); + + when(scheduleRepository.findById(schedule.getId())).thenReturn(Optional.of(schedule)); + when(quizRepository.findAllByScheduleIdAndCreatedAtBetween(schedule.getId(), date.atStartOfDay(), date.atStartOfDay().plusDays(1))) + .thenReturn(List.of(quiz)); + when(memberAttendanceRepository.findByQuizIdAndMemberId(quiz.getId(), member1.getId())) + .thenReturn(List.of(member1Attendance)); + when(memberAttendanceRepository.findByQuizIdAndMemberId(quiz.getId(), owner.getId())) + .thenReturn(List.of(ownerAttendance)); + } + + @Test + @DisplayName("회원 출석부 불러오기 - (성공)") + void getAllAttendances_Success() { + + // given + Long studyId = 1L; + + // 사용자 인증 정보 생성 + getAuthentication(member1.getId()); + + // when + StudyQuizResponseDTO.AttendanceListDTO result = memberStudyQueryService.getAllAttendances(studyId, schedule.getId(), date); + + // then + assertThat(result).isNotNull(); + assertThat(result.getScheduleId()).isEqualTo(schedule.getId()); + assertThat(result.getQuizId()).isEqualTo(quiz.getId()); + assertThat(result.getStudyMembers()).size().isEqualTo(2); // 전체 인원 2명 + assertThat(result.getStudyMembers().stream() + .filter(StudyQuizResponseDTO.StudyMemberDTO::getIsAttending) + .toList()).size().isEqualTo(1); // 출석 인원 1명 + } + + @Test + @DisplayName("회원 출석부 불러오기 - 스터디가 존재하지 않는 경우 (실패)") + void getAllAttendances_StudyNotFound_Fail() { + + // given + Long studyId = 2L; + + // 사용자 인증 정보 생성 + getAuthentication(member2.getId()); + + // when & then + assertThrows(StudyHandler.class, () -> memberStudyQueryService.getAllAttendances(studyId, schedule.getId(), date)); + } + + @Test + @DisplayName("회원 출석부 불러오기 - 스터디 회원이 아닌 경우 (실패)") + void getAllAttendances_NotStudyMember_Fail() { + + // given + Long studyId = 1L; + + // 사용자 인증 정보 생성 + getAuthentication(member2.getId()); + + // when & then + assertThrows(StudyHandler.class, () -> memberStudyQueryService.getAllAttendances(studyId, schedule.getId(), date)); + } + + @Test + @DisplayName("회원 출석부 불러오기 - 일정이 존재하지 않는 경우 (실패)") + void getAllAttendances_ScheduleNotFound_Fail() { + + // given + Long studyId = 1L; + + // 사용자 인증 정보 생성 + getAuthentication(member1.getId()); + + // when & then + assertThrows(StudyHandler.class, () -> memberStudyQueryService.getAllAttendances(studyId, 2L, date)); + } + + @Test + @DisplayName("출석 퀴즈 불러오기 - (성공)") + void getAttendanceQuiz_Success() { + + // given + Long studyId = 1L; + + // 사용자 인증 정보 생성 + getAuthentication(member1.getId()); + + // when + StudyQuizResponseDTO.QuizDTO result = memberStudyQueryService.getAttendanceQuiz(studyId, schedule.getId(), date); + + // then + assertThat(result).isNotNull(); + assertThat(result.getQuizId()).isEqualTo(quiz.getId()); + assertThat(result.getQuestion()).isEqualTo("최고의 스터디 앱은?"); + } + + @Test + @DisplayName("출석 퀴즈 불러오기 - 스터디가 존재하지 않는 경우 (실패)") + void getAttendanceQuiz_StudyNotFound_Fail() { + + // given + Long studyId = 2L; + + // 사용자 인증 정보 생성 + getAuthentication(member1.getId()); + + // when + assertThrows(StudyHandler.class, () -> memberStudyQueryService.getAttendanceQuiz(studyId, schedule.getId(), date)); + } + + @Test + @DisplayName("출석 퀴즈 불러오기 - 스터디 회원이 아닌 경우 (실패)") + void getAttendanceQuiz_NotStudyMember_Fail() { + + // given + Long studyId = 1L; + + // 사용자 인증 정보 생성 + getAuthentication(member2.getId()); + + // when + assertThrows(StudyHandler.class, () -> memberStudyQueryService.getAttendanceQuiz(studyId, schedule.getId(), date)); + } + + @Test + @DisplayName("출석 퀴즈 불러오기 - 일정이 존재하지 않는 경우 (실패)") + void getAttendanceQuiz_ScheduleNotFound_Fail() { + + // given + Long studyId = 1L; + + // 사용자 인증 정보 생성 + getAuthentication(member1.getId()); + + // when + assertThrows(StudyHandler.class, () -> memberStudyQueryService.getAttendanceQuiz(studyId, 2L, date)); + } + + +/*-------------------------------------------------------- Utils ------------------------------------------------------------------------*/ + + private static void initMember() { + member1 = Member.builder() + .id(1L) + .scheduleList(new ArrayList<>()) + .build(); + member2 = Member.builder() + .id(2L) + .scheduleList(new ArrayList<>()) + .build(); + owner = Member.builder() + .id(3L) + .scheduleList(new ArrayList<>()) + .build(); + } + + private static void initStudy() { + study = Study.builder() + .gender(Gender.MALE) + .minAge(20) + .maxAge(29) + .fee(10000) + .profileImage("a.jpg") + .hasFee(true) + .isOnline(true) + .goal("SQLD") + .introduction("SQLD 자격증 스터디") + .title("SQLD Master") + .maxPeople(10L) + .build(); + } + + private static void initMemberStudy() { + ownerStudy = MemberStudy.builder() + .id(1L) + .status(ApplicationStatus.APPROVED) + .isOwned(true) + .introduction("Hi") + .member(owner) + .study(study) + .build(); + member1Study = MemberStudy.builder() + .id(2L) + .status(ApplicationStatus.APPROVED) + .isOwned(false) + .introduction("Hi") + .member(member1) + .study(study) + .build(); + } + + private static void initSchedule() { + schedule = Schedule.builder() + .id(1L) + .study(study) + .member(owner) + .build(); + study.addSchedule(schedule); + owner.addSchedule(schedule); + } + + private static void initQuiz() { + quiz = Quiz.builder() + .schedule(schedule) + .member(owner) + .question("최고의 스터디 앱은?") + .answer("SPOT") + .build(); + quiz.addMemberAttendance(member1Attendance); + quiz.addMemberAttendance(ownerAttendance); + } + + private static void initMemberAttendance() { + member1Attendance = MemberAttendance.builder() + .isCorrect(true) + .build(); + member1Attendance.setMember(member1); + + ownerAttendance = MemberAttendance.builder() + .isCorrect(false) + .build(); + ownerAttendance.setQuiz(quiz); + } + + private static void getAuthentication(Long memberId) { + String idString = String.valueOf(memberId); + Authentication authentication = new UsernamePasswordAuthenticationToken(idString, null, Collections.emptyList()); + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(authentication); + SecurityContextHolder.setContext(securityContext); + } +} \ No newline at end of file