diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index 03df7c30e..0d257d97e 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -63,12 +63,15 @@ public enum ErrorCode implements ResponseCode { */ INVALID_SEARCH_TYPE(HttpStatus.BAD_REQUEST, 900000,"알맞은 검색어 타입을 찾을 수 없습니다."), + /** * 100000 : room error */ ROOM_NOT_FOUND(HttpStatus.NOT_FOUND, 100000, "존재하지 않는 ROOM 입니다."), INVALID_ROOM_CREATE(HttpStatus.BAD_REQUEST, 100001, "유효하지 않은 ROOM 생성 요청 입니다."), - + ROOM_PASSWORD_MISMATCH(HttpStatus.BAD_REQUEST, 100002, "비밀번호가 일치하지 않습니다."), + ROOM_PASSWORD_NOT_REQUIRED(HttpStatus.BAD_REQUEST, 100003, "공개방은 비밀번호가 필요하지 않습니다."), + ROOM_RECRUITMENT_PERIOD_EXPIRED(HttpStatus.BAD_REQUEST, 100004, "모집기간이 만료된 방입니다."), /** * 110000 : vote error */ diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java b/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java index 6c9e1216c..841aaeae9 100644 --- a/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java +++ b/src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java @@ -1,10 +1,24 @@ package konkuk.thip.room.adapter.in.web; +import jakarta.validation.Valid; +import konkuk.thip.common.dto.BaseResponse; +import konkuk.thip.room.adapter.in.web.request.RoomVerifyPasswordRequest; +import konkuk.thip.room.application.port.in.RoomVerifyPasswordUseCase; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor public class RoomQueryController { + private final RoomVerifyPasswordUseCase roomVerifyPasswordUseCase; + + //비공개 방 비밀번호 입력 검증 + @PostMapping("/rooms/{roomId}/password") + public BaseResponse verifyRoomPassword(@PathVariable("roomId") final Long roomId, + @Valid @RequestBody final RoomVerifyPasswordRequest roomVerifyPasswordRequest + ) { + return BaseResponse.ok(roomVerifyPasswordUseCase.verifyRoomPassword(roomVerifyPasswordRequest.toQuery(roomId))); + } + } diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/request/RoomVerifyPasswordRequest.java b/src/main/java/konkuk/thip/room/adapter/in/web/request/RoomVerifyPasswordRequest.java new file mode 100644 index 000000000..b97ca1647 --- /dev/null +++ b/src/main/java/konkuk/thip/room/adapter/in/web/request/RoomVerifyPasswordRequest.java @@ -0,0 +1,18 @@ +package konkuk.thip.room.adapter.in.web.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import konkuk.thip.room.application.port.in.dto.RoomVerifyPasswordQuery; + +public record RoomVerifyPasswordRequest( + + @NotBlank(message = "비밀번호는 필수입니다.") + @Pattern(regexp = "\\d{4}", message = "비밀번호는 숫자 4자리여야 합니다.") + String password +) { + public RoomVerifyPasswordQuery toQuery(Long roomId) { + return new RoomVerifyPasswordQuery( + roomId, + password); + } +} diff --git a/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java b/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java index 5426f28e2..9deb47ef8 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java +++ b/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java @@ -28,7 +28,6 @@ public class RoomJpaEntity extends BaseJpaEntity { @Column(name = "is_public", nullable = false) private boolean isPublic; - @Column(length = 4) private String password; @Builder.Default diff --git a/src/main/java/konkuk/thip/room/application/port/in/RoomVerifyPasswordUseCase.java b/src/main/java/konkuk/thip/room/application/port/in/RoomVerifyPasswordUseCase.java new file mode 100644 index 000000000..975f4d00f --- /dev/null +++ b/src/main/java/konkuk/thip/room/application/port/in/RoomVerifyPasswordUseCase.java @@ -0,0 +1,7 @@ +package konkuk.thip.room.application.port.in; + +import konkuk.thip.room.application.port.in.dto.RoomVerifyPasswordQuery; + +public interface RoomVerifyPasswordUseCase { + Void verifyRoomPassword(RoomVerifyPasswordQuery query); +} diff --git a/src/main/java/konkuk/thip/room/application/port/in/dto/DummyQuery.java b/src/main/java/konkuk/thip/room/application/port/in/dto/DummyQuery.java deleted file mode 100644 index f60120b60..000000000 --- a/src/main/java/konkuk/thip/room/application/port/in/dto/DummyQuery.java +++ /dev/null @@ -1,9 +0,0 @@ -package konkuk.thip.room.application.port.in.dto; - -import lombok.Builder; -import lombok.Getter; - -@Builder -@Getter -public class DummyQuery { -} diff --git a/src/main/java/konkuk/thip/room/application/port/in/dto/RoomVerifyPasswordQuery.java b/src/main/java/konkuk/thip/room/application/port/in/dto/RoomVerifyPasswordQuery.java new file mode 100644 index 000000000..aeb35aa5f --- /dev/null +++ b/src/main/java/konkuk/thip/room/application/port/in/dto/RoomVerifyPasswordQuery.java @@ -0,0 +1,7 @@ +package konkuk.thip.room.application.port.in.dto; + +public record RoomVerifyPasswordQuery( + Long roomId, + String password +) { +} diff --git a/src/main/java/konkuk/thip/room/application/service/RoomVerifyPasswordService.java b/src/main/java/konkuk/thip/room/application/service/RoomVerifyPasswordService.java new file mode 100644 index 000000000..88ead3713 --- /dev/null +++ b/src/main/java/konkuk/thip/room/application/service/RoomVerifyPasswordService.java @@ -0,0 +1,28 @@ +package konkuk.thip.room.application.service; + +import konkuk.thip.room.application.port.in.RoomVerifyPasswordUseCase; +import konkuk.thip.room.application.port.in.dto.RoomVerifyPasswordQuery; +import konkuk.thip.room.application.port.out.RoomCommandPort; +import konkuk.thip.room.domain.Room; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class RoomVerifyPasswordService implements RoomVerifyPasswordUseCase { + + private final RoomCommandPort roomCommandPort; + + @Override + public Void verifyRoomPassword(RoomVerifyPasswordQuery query) { + + //방 검증 + Room room = roomCommandPort.findById(query.roomId()); + + //도메인에서 비밀번호 검증 로직 수행 + room.verifyPassword(query.password()); + return null; + } +} diff --git a/src/main/java/konkuk/thip/room/domain/Room.java b/src/main/java/konkuk/thip/room/domain/Room.java index d630ce4b4..503bdf83c 100644 --- a/src/main/java/konkuk/thip/room/domain/Room.java +++ b/src/main/java/konkuk/thip/room/domain/Room.java @@ -3,6 +3,8 @@ import konkuk.thip.common.entity.BaseDomainEntity; import konkuk.thip.common.exception.InvalidStateException; import konkuk.thip.common.entity.StatusType; +import konkuk.thip.common.exception.BusinessException; +import konkuk.thip.common.exception.code.ErrorCode; import lombok.Getter; import lombok.experimental.SuperBuilder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -10,7 +12,7 @@ import java.time.LocalDate; -import static konkuk.thip.common.exception.code.ErrorCode.INVALID_ROOM_CREATE; +import static konkuk.thip.common.exception.code.ErrorCode.*; @Getter @SuperBuilder @@ -111,4 +113,33 @@ public boolean matchesPassword(String rawPassword) { } return PASSWORD_ENCODER.matches(rawPassword, this.hashedPassword); } + + public void verifyPassword(String rawPassword) { + + // 모집기간 만료 체크 + LocalDate deadline = this.startDate.minusDays(1); + if (isRecruitmentPeriodExpired()) { + String message = String.format("모집기간(%s까지)이 만료된 방에는 참여할 수 없습니다.", deadline); + throw new BusinessException( + ErrorCode.ROOM_RECRUITMENT_PERIOD_EXPIRED, new IllegalArgumentException(message) + ); + } + + // 공개방일 경우 비밀번호 입력 요청 예외 처리 + if (this.isPublic()) { + throw new BusinessException(ROOM_PASSWORD_NOT_REQUIRED); + } + + //비밀번호 검증 + if (!matchesPassword(rawPassword)) { + throw new BusinessException(ROOM_PASSWORD_MISMATCH); + } + } + + public boolean isRecruitmentPeriodExpired() { + LocalDate today = LocalDate.now(); + // 모집 마감일: startDate.minusDays(1) + return today.isAfter(this.startDate.minusDays(1)); + } + } diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomVerifyPasswordAPITest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomVerifyPasswordAPITest.java new file mode 100644 index 000000000..00147c327 --- /dev/null +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomVerifyPasswordAPITest.java @@ -0,0 +1,235 @@ +package konkuk.thip.room.adapter.in.web; + +import com.fasterxml.jackson.databind.ObjectMapper; +import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; +import konkuk.thip.book.adapter.out.persistence.BookJpaRepository; +import konkuk.thip.room.adapter.in.web.request.RoomVerifyPasswordRequest; +import konkuk.thip.room.adapter.out.jpa.CategoryJpaEntity; +import konkuk.thip.room.adapter.out.persistence.CategoryJpaRepository; +import konkuk.thip.user.adapter.out.jpa.AliasJpaEntity; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.jpa.UserRole; +import konkuk.thip.user.adapter.out.persistence.AliasJpaRepository; +import konkuk.thip.user.adapter.out.persistence.UserJpaRepository; +import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; +import konkuk.thip.room.adapter.out.persistence.RoomJpaRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + + +import java.time.LocalDate; + +import static konkuk.thip.common.exception.code.ErrorCode.*; +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@ActiveProfiles("test") +@AutoConfigureMockMvc(addFilters = false) +@DisplayName("[통합] 비공개 방 비밀번호 입력 검증 api 통합 테스트") +class RoomVerifyPasswordAPITest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private UserJpaRepository userJpaRepository; + + @Autowired + private AliasJpaRepository aliasJpaRepository; + + @Autowired + private BookJpaRepository bookJpaRepository; + + @Autowired + private CategoryJpaRepository categoryJpaRepository; + + @Autowired + private RoomJpaRepository roomJpaRepository; + + @Autowired + private ObjectMapper objectMapper; + + private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); + + private Long userId; + private Long privateRoomId; + private Long publicRoomId; + + @BeforeEach + void setUp() { + + AliasJpaEntity alias = aliasJpaRepository.save(AliasJpaEntity.builder() + .value("책벌레") + .color("blue") + .imageUrl("http://image.url") + .build()); + + UserJpaEntity user = userJpaRepository.save(UserJpaEntity.builder() + .oauth2Id("kakao_432708231") + .nickname("User1") + .imageUrl("https://avatar1.jpg") + .role(UserRole.USER) + .aliasForUserJpaEntity(alias) + .build()); + + BookJpaEntity book = bookJpaRepository.save(BookJpaEntity.builder() + .isbn("1234567890123") + .title("테스트책") + .imageUrl("https://image.url") + .authorName("저자") + .publisher("출판사") + .description("설명") + .bestSeller(false) + .build()); + userId = user.getUserId(); + + CategoryJpaEntity categoryJpaEntity = categoryJpaRepository.save(CategoryJpaEntity.builder() + .value("소설") + .aliasForCategoryJpaEntity(alias) + .build()); + + // 비공개 방 저장 (비밀번호: 1234) + RoomJpaEntity privateRoom = roomJpaRepository.save( + RoomJpaEntity.builder() + .bookJpaEntity(book) + .categoryJpaEntity(categoryJpaEntity) + .title("비공개방") + .description("비공개방입니다") + .isPublic(false) + .password(PASSWORD_ENCODER.encode("1234")) + .startDate(LocalDate.now().plusDays(1)) + .endDate(LocalDate.now().plusDays(5)) + .recruitCount(3) + .build() + ); + privateRoomId = privateRoom.getRoomId(); + + // 공개 방 저장 + RoomJpaEntity publicRoom = roomJpaRepository.save( + RoomJpaEntity.builder() + .bookJpaEntity(book) + .categoryJpaEntity(categoryJpaEntity) + .title("공개방") + .description("공개방입니다") + .isPublic(true) + .password(null) + .startDate(LocalDate.now().plusDays(1)) + .endDate(LocalDate.now().plusDays(5)) + .recruitCount(3) + .build() + ); + publicRoomId = publicRoom.getRoomId(); + } + + @AfterEach + void tearDown() { + roomJpaRepository.deleteAll(); + bookJpaRepository.deleteAll(); + userJpaRepository.deleteAll(); + categoryJpaRepository.deleteAll(); + aliasJpaRepository.deleteAll(); + } + + @Test + @DisplayName("모집기간이 만료되지 않은 비공개 방의 비밀번호 입력 검증에 [성공]한다") + void verifyRoomPassword_success() throws Exception { + + // given + RoomVerifyPasswordRequest request = new RoomVerifyPasswordRequest("1234"); + + //when & then + mockMvc.perform(post("/rooms/{roomId}/password", privateRoomId) + .requestAttr("userId", userId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.isSuccess").value(true)); + } + + @Test + @DisplayName("모집기간이 만료되지 않은 비공개 방의 비밀번호 입력 검증 [실패]시 400 Bad Request 반환") + void verifyRoomPassword_mismatch() throws Exception { + + // given + RoomVerifyPasswordRequest request = new RoomVerifyPasswordRequest("9999"); + + //when & then + mockMvc.perform(post("/rooms/{roomId}/password", privateRoomId) + .requestAttr("userId", userId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.isSuccess").value(false)) + .andExpect(jsonPath("$.code").value(ROOM_PASSWORD_MISMATCH.getCode())) + .andExpect(jsonPath("$.message" , containsString("비밀번호가 일치하지 않습니다."))); + } + + @Test + @DisplayName("[모집기간이 만료된] 비공개 방에 비밀번호 입력 검증 시 400 Bad Request 반환") + void verifyRoomPassword_recruitmentPeriodExpired() throws Exception { + + // given + // 모집기간: startDate.minusDays(1) 이전이면 만료 + // startDate를 오늘로 설정해 모집기간이 이미 지난 상태로 저장 + RoomJpaEntity expiredRoom = roomJpaRepository.save( + RoomJpaEntity.builder() + .bookJpaEntity(bookJpaRepository.findAll().get(0)) + .categoryJpaEntity(categoryJpaRepository.findAll().get(0)) + .title("모집만료방") + .description("모집기간이 만료된 방입니다") + .isPublic(false) + .password(PASSWORD_ENCODER.encode("1234")) + .startDate(LocalDate.now()) // 오늘 시작이므로 모집기간은 어제까지 + .endDate(LocalDate.now().plusDays(5)) + .recruitCount(3) + .build() + ); + Long expiredRoomId = expiredRoom.getRoomId(); + + RoomVerifyPasswordRequest request = new RoomVerifyPasswordRequest("1234"); + + //when & then + mockMvc.perform(post("/rooms/{roomId}/password", expiredRoomId) + .requestAttr("userId", userId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.isSuccess").value(false)) + .andExpect(jsonPath("$.code").value(ROOM_RECRUITMENT_PERIOD_EXPIRED.getCode())) + .andExpect(jsonPath("$.message", containsString(("모집기간이 만료된 방입니다.")))); + } + + @Test + @DisplayName("[공개방]에 비밀번호 입력 검증 시 400 Bad Request 반환") + void verifyRoomPassword_publicRoom() throws Exception { + + // given + RoomVerifyPasswordRequest request = new RoomVerifyPasswordRequest("1234"); + + //when & then + mockMvc.perform(post("/rooms/{roomId}/password", publicRoomId) + .requestAttr("userId", userId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.isSuccess").value(false)) + .andExpect(jsonPath("$.code").value(ROOM_PASSWORD_NOT_REQUIRED.getCode())) + .andExpect(jsonPath("$.message", containsString("공개방은 비밀번호가 필요하지 않습니다"))); + } +} \ No newline at end of file diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomVerifyPasswordControllerTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomVerifyPasswordControllerTest.java new file mode 100644 index 000000000..e19882bbe --- /dev/null +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomVerifyPasswordControllerTest.java @@ -0,0 +1,99 @@ +package konkuk.thip.room.adapter.in.web; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.DisplayName; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + + +import java.util.HashMap; +import java.util.Map; + +import static konkuk.thip.common.exception.code.ErrorCode.API_INVALID_PARAM; +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@SpringBootTest +@ActiveProfiles("test") +@AutoConfigureMockMvc(addFilters = false) +@DisplayName("[단위] 비공개 방 비밀번호 입력 api controller 단위 테스트") +class RoomVerifyPasswordControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + private Map buildValidRequest() { + Map request = new HashMap<>(); + request.put("userId", 1L); + request.put("roomId", 1L); + request.put("password", "1234"); + return request; + } + + private void assertBad(Map req, String msg) throws Exception { + mockMvc.perform(post("/rooms/{roomId}/password", req.get("roomId")) + .requestAttr("userId", req.get("userId")) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(API_INVALID_PARAM.getCode())) + .andExpect(jsonPath("$.message", containsString(msg))); + } + + @Nested + @DisplayName("비밀번호 검증") + class PasswordValidation { + + @Test + @DisplayName("숫자로 구성되지 않았을 때 400 error") + void invalid_password() throws Exception { + Map req = buildValidRequest(); + req.put("password", "12ab"); + assertBad(req, "비밀번호는 숫자 4자리여야 합니다."); + } + + @Test + @DisplayName("4자리 숫자 아닐 때 400 error") + void short_password() throws Exception { + Map req = buildValidRequest(); + req.put("password", "123"); + assertBad(req, "비밀번호는 숫자 4자리여야 합니다."); + } + + @Test + @DisplayName("빈 문자열일 때 400 error") + void blank_description() throws Exception { + Map req = buildValidRequest(); + req.put("password", ""); + assertBad(req, "비밀번호는 필수입니다."); + } + } + + @Nested + @DisplayName("roomId 검증") + class RoomIdValidation { + @Test + @DisplayName("roomId가 없을 때 400 error") + void missing_roomId() throws Exception { + Map req = buildValidRequest(); + req.remove("roomId"); + mockMvc.perform(post("/rooms//password") + .requestAttr("userId", req.get("userId")) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().is4xxClientError()); + } + } +} diff --git a/src/test/java/konkuk/thip/room/domain/RoomTest.java b/src/test/java/konkuk/thip/room/domain/RoomTest.java index fd11dd10e..1c90e1910 100644 --- a/src/test/java/konkuk/thip/room/domain/RoomTest.java +++ b/src/test/java/konkuk/thip/room/domain/RoomTest.java @@ -1,6 +1,8 @@ package konkuk.thip.room.domain; +import konkuk.thip.common.exception.BusinessException; import konkuk.thip.common.exception.InvalidStateException; +import konkuk.thip.common.exception.code.ErrorCode; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -9,8 +11,9 @@ import java.time.LocalDate; import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.util.ReflectionTestUtils.setField; -@DisplayName("Room 단위 테스트") +@DisplayName("[단위] Room 단위 테스트") class RoomTest { private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); @@ -152,4 +155,88 @@ void matchesPassword_no_password() { ); assertFalse(room.matchesPassword("0000")); } + + @Test + @DisplayName("isRecruitmentPeriodExpired: 모집기간이 만료되지 않은 경우 false 반환") + void isRecruitmentPeriodExpired_not_expired() { + Room room = Room.withoutId( + "제목", "설명", false, "1234", + LocalDate.now().plusDays(3), LocalDate.now().plusDays(10), 5, 123L, 456L + ); + assertFalse(room.isRecruitmentPeriodExpired()); + } + + @Test + @DisplayName("isRecruitmentPeriodExpired: 모집기간이 오늘이 마감일인 경우 false 반환") + void isRecruitmentPeriodExpired_deadline_today() { + LocalDate start = LocalDate.now().plusDays(1); + Room room = Room.withoutId( + "제목", "설명", false, "1234", + start, start.plusDays(10), 5, 123L, 456L + ); + // 오늘이 모집마감일(startDate.minusDays(1))이면 false + assertFalse(room.isRecruitmentPeriodExpired()); + } + + @Test + @DisplayName("isRecruitmentPeriodExpired: 모집기간이 이미 만료된 경우 true 반환") + void isRecruitmentPeriodExpired_expired() { + LocalDate start = LocalDate.now().plusDays(1); + Room room = Room.withoutId( + "제목", "설명", false, "1234", + start, start.plusDays(10), 5, 123L, 456L + ); + setField(room, "startDate", today); // 모집기간 만료 상태를 강제로 만든 후 검증 + assertTrue(room.isRecruitmentPeriodExpired()); + } + + @Test + @DisplayName("verifyPassword: 모집기간 만료 시 BusinessException(ROOM_RECRUITMENT_PERIOD_EXPIRED) 발생") + void verifyPassword_recruitmentPeriodExpired() { + LocalDate startExpired = today.plusDays(2); + Room room = Room.withoutId( + "제목", "설명", false, "1234", + startExpired, END, 5, 123L, 456L + ); + setField(room, "startDate", today); // 모집기간 만료 상태를 강제로 만든 후 검증 + BusinessException ex = assertThrows(BusinessException.class, + () -> room.verifyPassword("1234")); + assertEquals(ErrorCode.ROOM_RECRUITMENT_PERIOD_EXPIRED, ex.getErrorCode()); + assertTrue(ex.getCause().getMessage().contains("모집기간")); + } + + @Test + @DisplayName("verifyPassword: 공개방에 비밀번호 입력 시 BusinessException(ROOM_PASSWORD_NOT_REQUIRED) 발생") + void verifyPassword_publicRoom() { + Room room = Room.withoutId( + "제목", "설명", true, null, + START, END, 5, 123L, 456L + ); + BusinessException ex = assertThrows(BusinessException.class, + () -> room.verifyPassword("1234")); + assertEquals(ErrorCode.ROOM_PASSWORD_NOT_REQUIRED, ex.getErrorCode()); + } + + @Test + @DisplayName("verifyPassword: 비밀번호 불일치 시 BusinessException(ROOM_PASSWORD_MISMATCH) 발생") + void verifyPassword_passwordMismatch() { + Room room = Room.withoutId( + "제목", "설명", false, "1234", + START, END, 5, 123L, 456L + ); + BusinessException ex = assertThrows(BusinessException.class, + () -> room.verifyPassword("0000")); + assertEquals(ErrorCode.ROOM_PASSWORD_MISMATCH, ex.getErrorCode()); + } + + @Test + @DisplayName("verifyPassword: 모집기간 내, 비공개방, 비밀번호 일치 시 예외 발생하지 않음") + void verifyPassword_success() { + Room room = Room.withoutId( + "제목", "설명", false, "1234", + START, END, 5, 123L, 456L + ); + assertDoesNotThrow(() -> room.verifyPassword("1234")); + } + }