Skip to content

Commit a70ae7f

Browse files
authored
Merge pull request #291 from DevKor-github/feat/implement-issue-272
[codex] Secure schedule finish authorization
2 parents 7be5737 + 49af81b commit a70ae7f

5 files changed

Lines changed: 152 additions & 28 deletions

File tree

ontime-back/src/main/java/devkor/ontime_back/controller/ScheduleController.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ public ResponseEntity<ApiResponseForm<List<PreparationDto>>> getPreparation(Http
282282
content = @Content(
283283
schema = @Schema(
284284
type = "object",
285-
example = "{\"scheduleId\": \"3fa85f64-5717-4562-b3fc-2c963f66afe5\", \"latenessTime\": 3}"
285+
example = "{\"latenessTime\": 3}"
286286
)
287287
)
288288
)
@@ -299,10 +299,11 @@ public ResponseEntity<ApiResponseForm<List<PreparationDto>>> getPreparation(Http
299299
@PutMapping("/{scheduleId}/finish") // 약속 준비 종료 이후 지각시간(Schedule 테이블), 성실도 점수(User 테이블) 업데이트
300300
public ResponseEntity<ApiResponseForm<?>> finishSchedule(
301301
HttpServletRequest request,
302+
@PathVariable UUID scheduleId,
302303
@RequestBody FinishPreparationDto finishPreparationDto) {
303304

304305
Long userId = userAuthService.getUserIdFromToken(request);
305-
scheduleService.finishSchedule(userId, finishPreparationDto);
306+
scheduleService.finishSchedule(userId, scheduleId, finishPreparationDto);
306307
String message = "지각시간과 성실도점수가 성공적으로 업데이트 되었습니다!";
307308
return ResponseEntity.ok(ApiResponseForm.success(null, message));
308309
}

ontime-back/src/main/java/devkor/ontime_back/response/ErrorCode.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ public enum ErrorCode {
3030
FIRST_PREPARATION_NOT_FOUND(1012, "해당 ID의 사용자의 준비과정을 찾을 수 없습니다.", HttpStatus.BAD_REQUEST),
3131
NOTIFICATION_NOT_FOUND(1013, "알림을 찾을 수 없습니다.", HttpStatus.BAD_REQUEST ),
3232
PREPARATION_ALREADY_EXISTS(1014, "해당 사용자의 준비과정이 이미 존재합니다.", HttpStatus.BAD_REQUEST),
33+
SCHEDULE_ALREADY_FINISHED(1015, "이미 종료된 약속입니다.", HttpStatus.BAD_REQUEST),
34+
SCHEDULE_ID_MISMATCH(1016, "경로의 scheduleId와 요청 본문의 scheduleId가 일치하지 않습니다.", HttpStatus.BAD_REQUEST),
3335
ALARM_SETTINGS_INVALID_FIELD(1101, "ALARM_SETTINGS_INVALID_FIELD", HttpStatus.BAD_REQUEST),
3436
ALARM_WINDOW_RANGE_TOO_LONG(1102, "ALARM_WINDOW_RANGE_TOO_LONG", HttpStatus.BAD_REQUEST),
3537
DEVICE_SESSION_NOT_ACTIVE(1103, "DEVICE_SESSION_NOT_ACTIVE", HttpStatus.CONFLICT),

ontime-back/src/main/java/devkor/ontime_back/service/ScheduleService.java

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -173,20 +173,28 @@ public List<LatenessHistoryResponse> getLatenessHistory(Long userId) {
173173

174174
// 지각 시간 업데이트
175175
@Transactional
176-
public void updateLatenessTime(FinishPreparationDto finishPreparationDto) {
177-
UUID scheduleId = finishPreparationDto.getScheduleId();
178-
Integer latenessTime = finishPreparationDto.getLatenessTime();
179-
180-
Schedule schedule = scheduleRepository.findById(scheduleId)
181-
.orElseThrow(() -> new GeneralException(SCHEDULE_NOT_FOUND));
182-
176+
public void updateLatenessTime(Schedule schedule, Integer latenessTime) {
183177
schedule.updateLatenessTime(latenessTime);
184178
scheduleRepository.save(schedule);
185179
}
186180

187181
@Transactional
188-
public void finishSchedule(Long userId, FinishPreparationDto finishPreparationDto) {
189-
updateLatenessTime(finishPreparationDto);
182+
public void finishSchedule(Long userId, UUID scheduleId, FinishPreparationDto finishPreparationDto) {
183+
if (finishPreparationDto == null || finishPreparationDto.getLatenessTime() == null) {
184+
throw new GeneralException(INVALID_INPUT);
185+
}
186+
if (finishPreparationDto.getScheduleId() != null && !scheduleId.equals(finishPreparationDto.getScheduleId())) {
187+
throw new GeneralException(SCHEDULE_ID_MISMATCH);
188+
}
189+
190+
Schedule schedule = getScheduleWithAuthorization(scheduleId, userId);
191+
boolean alreadyFinishedByDoneStatus = schedule.getDoneStatus() != null && schedule.getDoneStatus() != DoneStatus.NOT_ENDED;
192+
boolean alreadyFinishedByLatenessTime = schedule.getLatenessTime() != null && schedule.getLatenessTime() != -1;
193+
if (alreadyFinishedByDoneStatus || alreadyFinishedByLatenessTime) {
194+
throw new GeneralException(SCHEDULE_ALREADY_FINISHED);
195+
}
196+
197+
updateLatenessTime(schedule, finishPreparationDto.getLatenessTime());
190198
userService.updatePunctualityScore(userId, finishPreparationDto.getLatenessTime());
191199
}
192200

ontime-back/src/test/java/devkor/ontime_back/controller/ScheduleControllerTest.java

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import devkor.ontime_back.TestSecurityConfig;
88
import devkor.ontime_back.dto.*;
99
import devkor.ontime_back.entity.DoneStatus;
10+
import devkor.ontime_back.response.ErrorCode;
11+
import devkor.ontime_back.response.GeneralException;
1012
import jakarta.servlet.http.HttpServletRequest;
1113
import org.junit.jupiter.api.DisplayName;
1214
import org.junit.jupiter.api.Test;
@@ -261,16 +263,18 @@ void getLatenessHistory() throws Exception {
261263
@Test
262264
void finishSchedule() throws Exception {
263265
// given
266+
Long userId = 1L;
267+
UUID scheduleId = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe5");
264268
FinishPreparationDto finishPreparationDto = FinishPreparationDto.builder()
265-
.scheduleId(UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe5"))
269+
.latenessTime(0)
266270
.build();
267271

268-
when(userAuthService.getUserIdFromToken(any())).thenReturn(1L);
269-
doNothing().when(scheduleService).finishSchedule(any(Long.class), any(FinishPreparationDto.class));
272+
when(userAuthService.getUserIdFromToken(any())).thenReturn(userId);
273+
doNothing().when(scheduleService).finishSchedule(eq(userId), eq(scheduleId), any(FinishPreparationDto.class));
270274

271275
// when // then
272276
mockMvc.perform(
273-
put("/schedules/" + 1L + "/finish")
277+
put("/schedules/{scheduleId}/finish", scheduleId)
274278
.content(objectMapper.writeValueAsString(finishPreparationDto))
275279
.contentType(MediaType.APPLICATION_JSON)
276280
)
@@ -279,5 +283,35 @@ void finishSchedule() throws Exception {
279283
.andExpect(jsonPath("$.code").value("200"))
280284
.andExpect(jsonPath("$.status").value("success"))
281285
.andExpect(jsonPath("$.message").value("지각시간과 성실도점수가 성공적으로 업데이트 되었습니다!"));
286+
287+
verify(scheduleService, times(1)).finishSchedule(eq(userId), eq(scheduleId), any(FinishPreparationDto.class));
288+
}
289+
290+
@DisplayName("약속 종료 요청에서 경로와 본문의 scheduleId가 다르면 400을 반환한다.")
291+
@Test
292+
void finishSchedule_failByScheduleIdMismatch() throws Exception {
293+
// given
294+
Long userId = 1L;
295+
UUID pathScheduleId = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe5");
296+
FinishPreparationDto finishPreparationDto = FinishPreparationDto.builder()
297+
.scheduleId(UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe6"))
298+
.latenessTime(0)
299+
.build();
300+
301+
when(userAuthService.getUserIdFromToken(any())).thenReturn(userId);
302+
doThrow(new GeneralException(ErrorCode.SCHEDULE_ID_MISMATCH))
303+
.when(scheduleService).finishSchedule(eq(userId), eq(pathScheduleId), any(FinishPreparationDto.class));
304+
305+
// when // then
306+
mockMvc.perform(
307+
put("/schedules/{scheduleId}/finish", pathScheduleId)
308+
.content(objectMapper.writeValueAsString(finishPreparationDto))
309+
.contentType(MediaType.APPLICATION_JSON)
310+
)
311+
.andDo(print())
312+
.andExpect(status().isBadRequest())
313+
.andExpect(jsonPath("$.code").value(ErrorCode.SCHEDULE_ID_MISMATCH.getCode()))
314+
.andExpect(jsonPath("$.status").value("error"))
315+
.andExpect(jsonPath("$.message").value(ErrorCode.SCHEDULE_ID_MISMATCH.getMessage()));
282316
}
283-
}
317+
}

ontime-back/src/test/java/devkor/ontime_back/service/ScheduleServiceTest.java

Lines changed: 91 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1233,16 +1233,17 @@ void updateLatenessTime(){
12331233
.build();
12341234

12351235
// when
1236-
scheduleService.updateLatenessTime(finishPreparationDto);
1236+
Schedule schedule = scheduleRepository.findById(finishPreparationDto.getScheduleId()).get();
1237+
scheduleService.updateLatenessTime(schedule, finishPreparationDto.getLatenessTime());
12371238

12381239
// then
12391240
assertThat(scheduleRepository.findById(UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe5"))
12401241
.get().getLatenessTime()).isEqualTo(1);
12411242
}
12421243

1243-
@DisplayName("지각 시간 업데이트할 때, 잘못된 schedulId를 DTO에 담아 요청하는 경우 예외가 발생한다.")
1244+
@DisplayName("약속 종료할 때, 경로와 본문의 scheduleId가 다르면 예외가 발생한다.")
12441245
@Test
1245-
void updateLatenessTimeWithWrongScheduleId(){
1246+
void finishScheduleWithScheduleIdMismatch(){
12461247
// given
12471248
User addedUser = User.builder()
12481249
.email("user@example.com")
@@ -1269,9 +1270,19 @@ void updateLatenessTimeWithWrongScheduleId(){
12691270
.build();
12701271

12711272
// when // then
1272-
assertThatThrownBy(() -> scheduleService.updateLatenessTime(finishPreparationDto))
1273+
assertThatThrownBy(() -> scheduleService.finishSchedule(
1274+
addedUser.getId(),
1275+
UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe5"),
1276+
finishPreparationDto
1277+
))
12731278
.isInstanceOf(GeneralException.class)
1274-
.hasMessage("해당 약속이 존재하지 않습니다.");
1279+
.hasMessage(ErrorCode.SCHEDULE_ID_MISMATCH.getMessage())
1280+
.extracting("errorCode")
1281+
.isEqualTo(ErrorCode.SCHEDULE_ID_MISMATCH);
1282+
1283+
assertThat(scheduleRepository.findById(UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe5"))
1284+
.get().getLatenessTime()).isEqualTo(-1);
1285+
assertThat(userRepository.findById(addedUser.getId()).get().getPunctualityScore()).isEqualTo(-1f);
12751286
}
12761287

12771288
@DisplayName("약속을 종료해 지각시간과 성실도점수 업데이트에 성공한다.")
@@ -1298,12 +1309,11 @@ void finishSchedule(){
12981309
scheduleRepository.save(addedSchedule);
12991310

13001311
FinishPreparationDto finishPreparationDto = FinishPreparationDto.builder()
1301-
.scheduleId(UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe5"))
13021312
.latenessTime(1)
13031313
.build();
13041314

13051315
// when
1306-
scheduleService.finishSchedule(addedUser.getId(), finishPreparationDto);
1316+
scheduleService.finishSchedule(addedUser.getId(), addedSchedule.getScheduleId(), finishPreparationDto);
13071317

13081318
// then
13091319
assertThat(scheduleRepository.findById(UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe5"))
@@ -1325,6 +1335,16 @@ void finishScheduleWithWrongUserId(){
13251335
.build();
13261336
userRepository.save(addedUser);
13271337

1338+
User otherUser = User.builder()
1339+
.email("other@example.com")
1340+
.password(passwordEncoder.encode("password1234"))
1341+
.name("other")
1342+
.punctualityScore(-1f)
1343+
.scheduleCountAfterReset(0)
1344+
.latenessCountAfterReset(0)
1345+
.build();
1346+
userRepository.save(otherUser);
1347+
13281348
Schedule addedSchedule = Schedule.builder()
13291349
.scheduleId(UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe5"))
13301350
.scheduleName("을사년 새해")
@@ -1340,9 +1360,15 @@ void finishScheduleWithWrongUserId(){
13401360
.build();
13411361

13421362
// when // then
1343-
assertThatThrownBy(() -> scheduleService.finishSchedule(addedUser.getId() + 1, finishPreparationDto))
1344-
.isInstanceOf(IllegalArgumentException.class)
1345-
.hasMessage("존재하지 않는 유저 id입니다.");
1363+
assertThatThrownBy(() -> scheduleService.finishSchedule(otherUser.getId(), addedSchedule.getScheduleId(), finishPreparationDto))
1364+
.isInstanceOf(GeneralException.class)
1365+
.hasMessage(ErrorCode.UNAUTHORIZED_ACCESS.getMessage())
1366+
.extracting("errorCode")
1367+
.isEqualTo(ErrorCode.UNAUTHORIZED_ACCESS);
1368+
1369+
assertThat(scheduleRepository.findById(addedSchedule.getScheduleId()).get().getLatenessTime()).isEqualTo(-1);
1370+
assertThat(userRepository.findById(addedUser.getId()).get().getPunctualityScore()).isEqualTo(-1f);
1371+
assertThat(userRepository.findById(otherUser.getId()).get().getPunctualityScore()).isEqualTo(-1f);
13461372
}
13471373

13481374
@DisplayName("약속을 종료할 때, 잘못된 scheduleId를 인자로 넘기는 경우 예외가 발생한다.")
@@ -1374,9 +1400,62 @@ void finishScheduleWithWrongScheduleId(){
13741400
.build();
13751401

13761402
// when // then
1377-
assertThatThrownBy(() -> scheduleService.finishSchedule(addedUser.getId(), finishPreparationDto))
1403+
assertThatThrownBy(() -> scheduleService.finishSchedule(
1404+
addedUser.getId(),
1405+
UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe6"),
1406+
finishPreparationDto
1407+
))
1408+
.isInstanceOf(GeneralException.class)
1409+
.hasMessage(ErrorCode.SCHEDULE_NOT_FOUND.getMessage())
1410+
.extracting("errorCode")
1411+
.isEqualTo(ErrorCode.SCHEDULE_NOT_FOUND);
1412+
1413+
assertThat(scheduleRepository.findById(addedSchedule.getScheduleId()).get().getLatenessTime()).isEqualTo(-1);
1414+
assertThat(userRepository.findById(addedUser.getId()).get().getPunctualityScore()).isEqualTo(-1f);
1415+
}
1416+
1417+
@DisplayName("이미 종료된 약속을 다시 종료하면 예외가 발생하고 성실도점수를 다시 계산하지 않는다.")
1418+
@Test
1419+
void finishScheduleWithAlreadyFinishedSchedule(){
1420+
// given
1421+
User addedUser = User.builder()
1422+
.email("user@example.com")
1423+
.password(passwordEncoder.encode("password1234"))
1424+
.name("junbeom")
1425+
.punctualityScore(100f)
1426+
.scheduleCountAfterReset(1)
1427+
.latenessCountAfterReset(0)
1428+
.build();
1429+
userRepository.save(addedUser);
1430+
1431+
Schedule addedSchedule = Schedule.builder()
1432+
.scheduleId(UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afe5"))
1433+
.scheduleName("을사년 새해")
1434+
.scheduleTime(LocalDateTime.of(2025, 1, 1, 0, 0))
1435+
.latenessTime(0)
1436+
.doneStatus(DoneStatus.NORMAL)
1437+
.user(addedUser)
1438+
.build();
1439+
scheduleRepository.save(addedSchedule);
1440+
1441+
FinishPreparationDto finishPreparationDto = FinishPreparationDto.builder()
1442+
.scheduleId(addedSchedule.getScheduleId())
1443+
.latenessTime(1)
1444+
.build();
1445+
1446+
// when // then
1447+
assertThatThrownBy(() -> scheduleService.finishSchedule(addedUser.getId(), addedSchedule.getScheduleId(), finishPreparationDto))
13781448
.isInstanceOf(GeneralException.class)
1379-
.hasMessage("해당 약속이 존재하지 않습니다.");
1449+
.hasMessage(ErrorCode.SCHEDULE_ALREADY_FINISHED.getMessage())
1450+
.extracting("errorCode")
1451+
.isEqualTo(ErrorCode.SCHEDULE_ALREADY_FINISHED);
1452+
1453+
User user = userRepository.findById(addedUser.getId()).get();
1454+
Schedule schedule = scheduleRepository.findById(addedSchedule.getScheduleId()).get();
1455+
assertThat(schedule.getLatenessTime()).isEqualTo(0);
1456+
assertThat(user.getPunctualityScore()).isEqualTo(100f);
1457+
assertThat(user.getScheduleCountAfterReset()).isEqualTo(1);
1458+
assertThat(user.getLatenessCountAfterReset()).isEqualTo(0);
13801459
}
13811460

13821461
@Test

0 commit comments

Comments
 (0)