diff --git a/ontime-back/src/main/java/devkor/ontime_back/controller/ScheduleController.java b/ontime-back/src/main/java/devkor/ontime_back/controller/ScheduleController.java index 4fe6983..2c90367 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/controller/ScheduleController.java +++ b/ontime-back/src/main/java/devkor/ontime_back/controller/ScheduleController.java @@ -256,7 +256,7 @@ public ResponseEntity>> getPreparation(Http content = @Content( schema = @Schema( type = "object", - example = "{\"scheduleId\": \"3fa85f64-5717-4562-b3fc-2c963f66afe5\", \"latenessTime\": 3}" + example = "{\"latenessTime\": 3}" ) ) ) @@ -273,10 +273,11 @@ public ResponseEntity>> getPreparation(Http @PutMapping("/{scheduleId}/finish") // 약속 준비 종료 이후 지각시간(Schedule 테이블), 성실도 점수(User 테이블) 업데이트 public ResponseEntity> finishSchedule( HttpServletRequest request, + @PathVariable UUID scheduleId, @RequestBody FinishPreparationDto finishPreparationDto) { Long userId = userAuthService.getUserIdFromToken(request); - scheduleService.finishSchedule(userId, finishPreparationDto); + scheduleService.finishSchedule(userId, scheduleId, finishPreparationDto); String message = "지각시간과 성실도점수가 성공적으로 업데이트 되었습니다!"; return ResponseEntity.ok(ApiResponseForm.success(null, message)); } diff --git a/ontime-back/src/main/java/devkor/ontime_back/response/ErrorCode.java b/ontime-back/src/main/java/devkor/ontime_back/response/ErrorCode.java index 3117cab..366b15d 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/response/ErrorCode.java +++ b/ontime-back/src/main/java/devkor/ontime_back/response/ErrorCode.java @@ -30,6 +30,8 @@ public enum ErrorCode { FIRST_PREPARATION_NOT_FOUND(1012, "해당 ID의 사용자의 준비과정을 찾을 수 없습니다.", HttpStatus.BAD_REQUEST), NOTIFICATION_NOT_FOUND(1013, "알림을 찾을 수 없습니다.", HttpStatus.BAD_REQUEST ), PREPARATION_ALREADY_EXISTS(1014, "해당 사용자의 준비과정이 이미 존재합니다.", HttpStatus.BAD_REQUEST), + SCHEDULE_ALREADY_FINISHED(1015, "이미 종료된 약속입니다.", HttpStatus.BAD_REQUEST), + SCHEDULE_ID_MISMATCH(1016, "경로의 scheduleId와 요청 본문의 scheduleId가 일치하지 않습니다.", HttpStatus.BAD_REQUEST), ALARM_SETTINGS_INVALID_FIELD(1101, "ALARM_SETTINGS_INVALID_FIELD", HttpStatus.BAD_REQUEST), ALARM_WINDOW_RANGE_TOO_LONG(1102, "ALARM_WINDOW_RANGE_TOO_LONG", HttpStatus.BAD_REQUEST), DEVICE_SESSION_NOT_ACTIVE(1103, "DEVICE_SESSION_NOT_ACTIVE", HttpStatus.CONFLICT), diff --git a/ontime-back/src/main/java/devkor/ontime_back/service/ScheduleService.java b/ontime-back/src/main/java/devkor/ontime_back/service/ScheduleService.java index 172a997..5cfca01 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/service/ScheduleService.java +++ b/ontime-back/src/main/java/devkor/ontime_back/service/ScheduleService.java @@ -173,20 +173,28 @@ public List getLatenessHistory(Long userId) { // 지각 시간 업데이트 @Transactional - public void updateLatenessTime(FinishPreparationDto finishPreparationDto) { - UUID scheduleId = finishPreparationDto.getScheduleId(); - Integer latenessTime = finishPreparationDto.getLatenessTime(); - - Schedule schedule = scheduleRepository.findById(scheduleId) - .orElseThrow(() -> new GeneralException(SCHEDULE_NOT_FOUND)); - + public void updateLatenessTime(Schedule schedule, Integer latenessTime) { schedule.updateLatenessTime(latenessTime); scheduleRepository.save(schedule); } @Transactional - public void finishSchedule(Long userId, FinishPreparationDto finishPreparationDto) { - updateLatenessTime(finishPreparationDto); + public void finishSchedule(Long userId, UUID scheduleId, FinishPreparationDto finishPreparationDto) { + if (finishPreparationDto == null || finishPreparationDto.getLatenessTime() == null) { + throw new GeneralException(INVALID_INPUT); + } + if (finishPreparationDto.getScheduleId() != null && !scheduleId.equals(finishPreparationDto.getScheduleId())) { + throw new GeneralException(SCHEDULE_ID_MISMATCH); + } + + Schedule schedule = getScheduleWithAuthorization(scheduleId, userId); + boolean alreadyFinishedByDoneStatus = schedule.getDoneStatus() != null && schedule.getDoneStatus() != DoneStatus.NOT_ENDED; + boolean alreadyFinishedByLatenessTime = schedule.getLatenessTime() != null && schedule.getLatenessTime() != -1; + if (alreadyFinishedByDoneStatus || alreadyFinishedByLatenessTime) { + throw new GeneralException(SCHEDULE_ALREADY_FINISHED); + } + + updateLatenessTime(schedule, finishPreparationDto.getLatenessTime()); userService.updatePunctualityScore(userId, finishPreparationDto.getLatenessTime()); } diff --git a/ontime-back/src/test/java/devkor/ontime_back/controller/ScheduleControllerTest.java b/ontime-back/src/test/java/devkor/ontime_back/controller/ScheduleControllerTest.java index 3395de6..47f5571 100644 --- a/ontime-back/src/test/java/devkor/ontime_back/controller/ScheduleControllerTest.java +++ b/ontime-back/src/test/java/devkor/ontime_back/controller/ScheduleControllerTest.java @@ -7,6 +7,8 @@ import devkor.ontime_back.TestSecurityConfig; import devkor.ontime_back.dto.*; import devkor.ontime_back.entity.DoneStatus; +import devkor.ontime_back.response.ErrorCode; +import devkor.ontime_back.response.GeneralException; import jakarta.servlet.http.HttpServletRequest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -261,16 +263,18 @@ void getLatenessHistory() throws Exception { @Test void finishSchedule() throws Exception { // given + Long userId = 1L; + UUID scheduleId = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe5"); FinishPreparationDto finishPreparationDto = FinishPreparationDto.builder() - .scheduleId(UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe5")) + .latenessTime(0) .build(); - when(userAuthService.getUserIdFromToken(any())).thenReturn(1L); - doNothing().when(scheduleService).finishSchedule(any(Long.class), any(FinishPreparationDto.class)); + when(userAuthService.getUserIdFromToken(any())).thenReturn(userId); + doNothing().when(scheduleService).finishSchedule(eq(userId), eq(scheduleId), any(FinishPreparationDto.class)); // when // then mockMvc.perform( - put("/schedules/" + 1L + "/finish") + put("/schedules/{scheduleId}/finish", scheduleId) .content(objectMapper.writeValueAsString(finishPreparationDto)) .contentType(MediaType.APPLICATION_JSON) ) @@ -279,5 +283,35 @@ void finishSchedule() throws Exception { .andExpect(jsonPath("$.code").value("200")) .andExpect(jsonPath("$.status").value("success")) .andExpect(jsonPath("$.message").value("지각시간과 성실도점수가 성공적으로 업데이트 되었습니다!")); + + verify(scheduleService, times(1)).finishSchedule(eq(userId), eq(scheduleId), any(FinishPreparationDto.class)); + } + + @DisplayName("약속 종료 요청에서 경로와 본문의 scheduleId가 다르면 400을 반환한다.") + @Test + void finishSchedule_failByScheduleIdMismatch() throws Exception { + // given + Long userId = 1L; + UUID pathScheduleId = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe5"); + FinishPreparationDto finishPreparationDto = FinishPreparationDto.builder() + .scheduleId(UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe6")) + .latenessTime(0) + .build(); + + when(userAuthService.getUserIdFromToken(any())).thenReturn(userId); + doThrow(new GeneralException(ErrorCode.SCHEDULE_ID_MISMATCH)) + .when(scheduleService).finishSchedule(eq(userId), eq(pathScheduleId), any(FinishPreparationDto.class)); + + // when // then + mockMvc.perform( + put("/schedules/{scheduleId}/finish", pathScheduleId) + .content(objectMapper.writeValueAsString(finishPreparationDto)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(ErrorCode.SCHEDULE_ID_MISMATCH.getCode())) + .andExpect(jsonPath("$.status").value("error")) + .andExpect(jsonPath("$.message").value(ErrorCode.SCHEDULE_ID_MISMATCH.getMessage())); } -} \ No newline at end of file +} diff --git a/ontime-back/src/test/java/devkor/ontime_back/service/ScheduleServiceTest.java b/ontime-back/src/test/java/devkor/ontime_back/service/ScheduleServiceTest.java index 9b1887e..9f996d7 100644 --- a/ontime-back/src/test/java/devkor/ontime_back/service/ScheduleServiceTest.java +++ b/ontime-back/src/test/java/devkor/ontime_back/service/ScheduleServiceTest.java @@ -1233,16 +1233,17 @@ void updateLatenessTime(){ .build(); // when - scheduleService.updateLatenessTime(finishPreparationDto); + Schedule schedule = scheduleRepository.findById(finishPreparationDto.getScheduleId()).get(); + scheduleService.updateLatenessTime(schedule, finishPreparationDto.getLatenessTime()); // then assertThat(scheduleRepository.findById(UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe5")) .get().getLatenessTime()).isEqualTo(1); } - @DisplayName("지각 시간 업데이트할 때, 잘못된 schedulId를 DTO에 담아 요청하는 경우 예외가 발생한다.") + @DisplayName("약속 종료할 때, 경로와 본문의 scheduleId가 다르면 예외가 발생한다.") @Test - void updateLatenessTimeWithWrongScheduleId(){ + void finishScheduleWithScheduleIdMismatch(){ // given User addedUser = User.builder() .email("user@example.com") @@ -1269,9 +1270,19 @@ void updateLatenessTimeWithWrongScheduleId(){ .build(); // when // then - assertThatThrownBy(() -> scheduleService.updateLatenessTime(finishPreparationDto)) + assertThatThrownBy(() -> scheduleService.finishSchedule( + addedUser.getId(), + UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe5"), + finishPreparationDto + )) .isInstanceOf(GeneralException.class) - .hasMessage("해당 약속이 존재하지 않습니다."); + .hasMessage(ErrorCode.SCHEDULE_ID_MISMATCH.getMessage()) + .extracting("errorCode") + .isEqualTo(ErrorCode.SCHEDULE_ID_MISMATCH); + + assertThat(scheduleRepository.findById(UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe5")) + .get().getLatenessTime()).isEqualTo(-1); + assertThat(userRepository.findById(addedUser.getId()).get().getPunctualityScore()).isEqualTo(-1f); } @DisplayName("약속을 종료해 지각시간과 성실도점수 업데이트에 성공한다.") @@ -1298,12 +1309,11 @@ void finishSchedule(){ scheduleRepository.save(addedSchedule); FinishPreparationDto finishPreparationDto = FinishPreparationDto.builder() - .scheduleId(UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe5")) .latenessTime(1) .build(); // when - scheduleService.finishSchedule(addedUser.getId(), finishPreparationDto); + scheduleService.finishSchedule(addedUser.getId(), addedSchedule.getScheduleId(), finishPreparationDto); // then assertThat(scheduleRepository.findById(UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe5")) @@ -1325,6 +1335,16 @@ void finishScheduleWithWrongUserId(){ .build(); userRepository.save(addedUser); + User otherUser = User.builder() + .email("other@example.com") + .password(passwordEncoder.encode("password1234")) + .name("other") + .punctualityScore(-1f) + .scheduleCountAfterReset(0) + .latenessCountAfterReset(0) + .build(); + userRepository.save(otherUser); + Schedule addedSchedule = Schedule.builder() .scheduleId(UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe5")) .scheduleName("을사년 새해") @@ -1340,9 +1360,15 @@ void finishScheduleWithWrongUserId(){ .build(); // when // then - assertThatThrownBy(() -> scheduleService.finishSchedule(addedUser.getId() + 1, finishPreparationDto)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("존재하지 않는 유저 id입니다."); + assertThatThrownBy(() -> scheduleService.finishSchedule(otherUser.getId(), addedSchedule.getScheduleId(), finishPreparationDto)) + .isInstanceOf(GeneralException.class) + .hasMessage(ErrorCode.UNAUTHORIZED_ACCESS.getMessage()) + .extracting("errorCode") + .isEqualTo(ErrorCode.UNAUTHORIZED_ACCESS); + + assertThat(scheduleRepository.findById(addedSchedule.getScheduleId()).get().getLatenessTime()).isEqualTo(-1); + assertThat(userRepository.findById(addedUser.getId()).get().getPunctualityScore()).isEqualTo(-1f); + assertThat(userRepository.findById(otherUser.getId()).get().getPunctualityScore()).isEqualTo(-1f); } @DisplayName("약속을 종료할 때, 잘못된 scheduleId를 인자로 넘기는 경우 예외가 발생한다.") @@ -1374,9 +1400,62 @@ void finishScheduleWithWrongScheduleId(){ .build(); // when // then - assertThatThrownBy(() -> scheduleService.finishSchedule(addedUser.getId(), finishPreparationDto)) + assertThatThrownBy(() -> scheduleService.finishSchedule( + addedUser.getId(), + UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe6"), + finishPreparationDto + )) + .isInstanceOf(GeneralException.class) + .hasMessage(ErrorCode.SCHEDULE_NOT_FOUND.getMessage()) + .extracting("errorCode") + .isEqualTo(ErrorCode.SCHEDULE_NOT_FOUND); + + assertThat(scheduleRepository.findById(addedSchedule.getScheduleId()).get().getLatenessTime()).isEqualTo(-1); + assertThat(userRepository.findById(addedUser.getId()).get().getPunctualityScore()).isEqualTo(-1f); + } + + @DisplayName("이미 종료된 약속을 다시 종료하면 예외가 발생하고 성실도점수를 다시 계산하지 않는다.") + @Test + void finishScheduleWithAlreadyFinishedSchedule(){ + // given + User addedUser = User.builder() + .email("user@example.com") + .password(passwordEncoder.encode("password1234")) + .name("junbeom") + .punctualityScore(100f) + .scheduleCountAfterReset(1) + .latenessCountAfterReset(0) + .build(); + userRepository.save(addedUser); + + Schedule addedSchedule = Schedule.builder() + .scheduleId(UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe5")) + .scheduleName("을사년 새해") + .scheduleTime(LocalDateTime.of(2025, 1, 1, 0, 0)) + .latenessTime(0) + .doneStatus(DoneStatus.NORMAL) + .user(addedUser) + .build(); + scheduleRepository.save(addedSchedule); + + FinishPreparationDto finishPreparationDto = FinishPreparationDto.builder() + .scheduleId(addedSchedule.getScheduleId()) + .latenessTime(1) + .build(); + + // when // then + assertThatThrownBy(() -> scheduleService.finishSchedule(addedUser.getId(), addedSchedule.getScheduleId(), finishPreparationDto)) .isInstanceOf(GeneralException.class) - .hasMessage("해당 약속이 존재하지 않습니다."); + .hasMessage(ErrorCode.SCHEDULE_ALREADY_FINISHED.getMessage()) + .extracting("errorCode") + .isEqualTo(ErrorCode.SCHEDULE_ALREADY_FINISHED); + + User user = userRepository.findById(addedUser.getId()).get(); + Schedule schedule = scheduleRepository.findById(addedSchedule.getScheduleId()).get(); + assertThat(schedule.getLatenessTime()).isEqualTo(0); + assertThat(user.getPunctualityScore()).isEqualTo(100f); + assertThat(user.getScheduleCountAfterReset()).isEqualTo(1); + assertThat(user.getLatenessCountAfterReset()).isEqualTo(0); } @Test