diff --git a/backend/src/main/java/kr/momo/controller/meeting/MeetingController.java b/backend/src/main/java/kr/momo/controller/meeting/MeetingController.java index ef3277105..760ff562d 100644 --- a/backend/src/main/java/kr/momo/controller/meeting/MeetingController.java +++ b/backend/src/main/java/kr/momo/controller/meeting/MeetingController.java @@ -48,4 +48,9 @@ public MomoApiResponse findMeetingSharing(@PathVariable public void lock(@PathVariable String uuid, @AuthAttendee long id) { meetingService.lock(uuid, id); } + + @PatchMapping("/api/v1/meetings/{uuid}/unlock") + public void unlock(@PathVariable String uuid, @AuthAttendee long id) { + meetingService.unlock(uuid, id); + } } diff --git a/backend/src/main/java/kr/momo/domain/meeting/Meeting.java b/backend/src/main/java/kr/momo/domain/meeting/Meeting.java index 982d591e8..d3c67d8ae 100644 --- a/backend/src/main/java/kr/momo/domain/meeting/Meeting.java +++ b/backend/src/main/java/kr/momo/domain/meeting/Meeting.java @@ -45,18 +45,22 @@ public Meeting(String name, String uuid, LocalTime firstTime, LocalTime lastTime } public void lock() { - this.isLocked = true; + isLocked = true; + } + + public void unlock() { + isLocked = false; } public Timeslot getValidatedTimeslot(LocalTime other) { - return this.timeslotInterval.getValidatedTimeslot(other); + return timeslotInterval.getValidatedTimeslot(other); } public LocalTime startTimeslotTime() { - return this.timeslotInterval.getStartTimeslot().getLocalTime(); + return timeslotInterval.getStartTimeslot().getLocalTime(); } public LocalTime endTimeslotTime() { - return this.timeslotInterval.getEndTimeslot().getLocalTime(); + return timeslotInterval.getEndTimeslot().getLocalTime(); } } diff --git a/backend/src/main/java/kr/momo/service/meeting/MeetingService.java b/backend/src/main/java/kr/momo/service/meeting/MeetingService.java index 32a4b2e3a..99d11e4e8 100644 --- a/backend/src/main/java/kr/momo/service/meeting/MeetingService.java +++ b/backend/src/main/java/kr/momo/service/meeting/MeetingService.java @@ -84,4 +84,14 @@ private void validateHostPermission(Attendee attendee) { throw new MomoException(AttendeeErrorCode.ACCESS_DENIED); } } + + @Transactional + public void unlock(String uuid, long id) { + Meeting meeting = meetingRepository.findByUuid(uuid) + .orElseThrow(() -> new MomoException(MeetingErrorCode.INVALID_UUID)); + Attendee attendee = attendeeRepository.findByIdAndMeeting(id, meeting) + .orElseThrow(() -> new MomoException(AttendeeErrorCode.INVALID_ATTENDEE)); + validateHostPermission(attendee); + meeting.unlock(); + } } diff --git a/backend/src/test/java/kr/momo/controller/meeting/MeetingControllerTest.java b/backend/src/test/java/kr/momo/controller/meeting/MeetingControllerTest.java index e23b457ae..860a51422 100644 --- a/backend/src/test/java/kr/momo/controller/meeting/MeetingControllerTest.java +++ b/backend/src/test/java/kr/momo/controller/meeting/MeetingControllerTest.java @@ -150,15 +150,7 @@ void createByDuplicatedName() { void lock() { Meeting meeting = meetingRepository.save(MeetingFixture.DINNER.create()); Attendee attendee = attendeeRepository.save(AttendeeFixture.HOST_JAZZ.create(meeting)); - AttendeeLoginRequest request = new AttendeeLoginRequest(attendee.name(), attendee.password()); - - String token = RestAssured.given().log().all() - .contentType(ContentType.JSON) - .body(request) - .when().post("/api/v1/meetings/{uuid}/login", meeting.getUuid()) - .then().log().all() - .statusCode(HttpStatus.OK.value()) - .extract().jsonPath().getString("data.token"); + String token = getToken(attendee, meeting); RestAssured.given().log().all() .contentType(ContentType.JSON) @@ -175,15 +167,7 @@ void lockWithInvalidUUID() { String invalidUUID = "INVALID_UUID"; Meeting meeting = meetingRepository.save(MeetingFixture.DINNER.create()); Attendee attendee = attendeeRepository.save(AttendeeFixture.HOST_JAZZ.create(meeting)); - AttendeeLoginRequest request = new AttendeeLoginRequest(attendee.name(), attendee.password()); - - String token = RestAssured.given().log().all() - .contentType(ContentType.JSON) - .body(request) - .when().post("/api/v1/meetings/{uuid}/login", meeting.getUuid()) - .then().log().all() - .statusCode(HttpStatus.OK.value()) - .extract().jsonPath().getString("data.token"); + String token = getToken(attendee, meeting); RestAssured.given().log().all() .contentType(ContentType.JSON) @@ -199,22 +183,75 @@ void lockWithInvalidUUID() { void lockWithNoPermission() { Meeting meeting = meetingRepository.save(MeetingFixture.DINNER.create()); Attendee attendee = attendeeRepository.save(AttendeeFixture.GUEST_PEDRO.create(meeting)); - AttendeeLoginRequest request = new AttendeeLoginRequest(attendee.name(), attendee.password()); + String token = getToken(attendee, meeting); - String token = RestAssured.given().log().all() + RestAssured.given().log().all() .contentType(ContentType.JSON) - .body(request) - .when().post("/api/v1/meetings/{uuid}/login", meeting.getUuid()) + .pathParam("uuid", meeting.getUuid()) + .header("Authorization", "Bearer " + token) + .when().patch("/api/v1/meetings/{uuid}/lock") .then().log().all() - .statusCode(HttpStatus.OK.value()) - .extract().jsonPath().getString("data.token"); + .statusCode(HttpStatus.FORBIDDEN.value()); + } + + @DisplayName("약속을 잠금을 해제하면 200 OK를 반환한다.") + @Test + void unlock() { + Meeting meeting = meetingRepository.save(MeetingFixture.DINNER.create()); + Attendee attendee = attendeeRepository.save(AttendeeFixture.HOST_JAZZ.create(meeting)); + String token = getToken(attendee, meeting); RestAssured.given().log().all() .contentType(ContentType.JSON) .pathParam("uuid", meeting.getUuid()) .header("Authorization", "Bearer " + token) - .when().patch("/api/v1/meetings/{uuid}/lock") + .when().patch("/api/v1/meetings/{uuid}/unlock") + .then().log().all() + .statusCode(HttpStatus.OK.value()); + } + + @DisplayName("존재하지 않는 약속을 잠금 해제 시도하면 400 Bad Request를 반환한다.") + @Test + void unlockWithInvalidUUID() { + String invalidUUID = "INVALID_UUID"; + Meeting meeting = meetingRepository.save(MeetingFixture.DINNER.create()); + Attendee attendee = attendeeRepository.save(AttendeeFixture.HOST_JAZZ.create(meeting)); + String token = getToken(attendee, meeting); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .pathParam("uuid", invalidUUID) + .header("Authorization", "Bearer " + token) + .when().patch("/api/v1/meetings/{uuid}/unlock") + .then().log().all() + .statusCode(HttpStatus.BAD_REQUEST.value()); + } + + @DisplayName("약속을 잠금을 해제할 때 호스트 권한이 없다면 403을 반환한다.") + @Test + void unlockWithNoPermission() { + Meeting meeting = meetingRepository.save(MeetingFixture.DINNER.create()); + Attendee attendee = attendeeRepository.save(AttendeeFixture.GUEST_PEDRO.create(meeting)); + String token = getToken(attendee, meeting); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .pathParam("uuid", meeting.getUuid()) + .header("Authorization", "Bearer " + token) + .when().patch("/api/v1/meetings/{uuid}/unlock") .then().log().all() .statusCode(HttpStatus.FORBIDDEN.value()); } + + private String getToken(Attendee attendee, Meeting meeting) { + AttendeeLoginRequest request = new AttendeeLoginRequest(attendee.name(), attendee.password()); + + return RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(request) + .when().post("/api/v1/meetings/{uuid}/login", meeting.getUuid()) + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().jsonPath().getString("data.token"); + } } diff --git a/backend/src/test/java/kr/momo/service/meeting/MeetingServiceTest.java b/backend/src/test/java/kr/momo/service/meeting/MeetingServiceTest.java index 25b9f5119..87349944c 100644 --- a/backend/src/test/java/kr/momo/service/meeting/MeetingServiceTest.java +++ b/backend/src/test/java/kr/momo/service/meeting/MeetingServiceTest.java @@ -157,4 +157,54 @@ void throwsExceptionWhenAttendeeGuest() { .isInstanceOf(MomoException.class) .hasMessage(AttendeeErrorCode.ACCESS_DENIED.message()); } + + @DisplayName("약속 잠금을 해제하면 잠금 상태가 변경된다.") + @Test + void unlock() { + Meeting meeting = meetingRepository.save(MeetingFixture.GAME.create()); + Attendee attendee = attendeeRepository.save(AttendeeFixture.HOST_JAZZ.create(meeting)); + + meetingService.unlock(meeting.getUuid(), attendee.getId()); + Meeting changedMeeting = meetingRepository.findById(meeting.getId()).orElseThrow(); + + assertThat(changedMeeting.isLocked()).isFalse(); + } + + @DisplayName("약속 잠금을 해제할 때 약속을 조회할 수 없다면 예외가 발생한다.") + @Test + void throwsExceptionWhenUnlockNoMeeting() { + Meeting meeting = meetingRepository.save(MeetingFixture.GAME.create()); + Attendee attendee = attendeeRepository.save(AttendeeFixture.GUEST_PEDRO.create(meeting)); + String uuid = ""; + long id = attendee.getId(); + + assertThatThrownBy(() -> meetingService.unlock(uuid, id)) + .isInstanceOf(MomoException.class) + .hasMessage(MeetingErrorCode.INVALID_UUID.message()); + } + + @DisplayName("약속 잠금을 해제할 때 참가자가 존재하지 않다면 예외가 발생한다.") + @Test + void throwsExceptionWhenUnlockNoAttendee() { + Meeting meeting = meetingRepository.save(MeetingFixture.GAME.create()); + String uuid = meeting.getUuid(); + long id = 1L; + + assertThatThrownBy(() -> meetingService.unlock(uuid, id)) + .isInstanceOf(MomoException.class) + .hasMessage(AttendeeErrorCode.INVALID_ATTENDEE.message()); + } + + @DisplayName("약속 잠금을 해제할 때 로그인된 참가자가 호스트가 아니면 예외가 발생한다.") + @Test + void throwsExceptionWhenUnlockAttendeeGuest() { + Meeting meeting = meetingRepository.save(MeetingFixture.GAME.create()); + Attendee attendee = attendeeRepository.save(AttendeeFixture.GUEST_PEDRO.create(meeting)); + String uuid = meeting.getUuid(); + long id = attendee.getId(); + + assertThatThrownBy(() -> meetingService.unlock(uuid, id)) + .isInstanceOf(MomoException.class) + .hasMessage(AttendeeErrorCode.ACCESS_DENIED.message()); + } }