diff --git a/src/main/kotlin/roomescape/time/business/TimeService.kt b/src/main/kotlin/roomescape/time/business/TimeService.kt deleted file mode 100644 index 42af422d..00000000 --- a/src/main/kotlin/roomescape/time/business/TimeService.kt +++ /dev/null @@ -1,74 +0,0 @@ -package roomescape.time.business - -import io.github.oshai.kotlinlogging.KotlinLogging -import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional -import roomescape.time.implement.TimeFinder -import roomescape.time.implement.TimeWriter -import roomescape.time.infrastructure.persistence.TimeEntity -import roomescape.time.web.* -import java.time.LocalDate -import java.time.LocalTime - -private val log = KotlinLogging.logger {} - -@Service -class TimeService( - private val timeFinder: TimeFinder, - private val timeWriter: TimeWriter, -) { - @Transactional(readOnly = true) - fun findById(id: Long): TimeEntity { - log.debug { "[TimeService.findById] 시작: timeId=$id" } - - return timeFinder.findById(id) - .also { log.info { "[TimeService.findById] 완료: timeId=$id, startAt=${it.startAt}" } } - } - - @Transactional(readOnly = true) - fun findTimes(): TimeRetrieveListResponse { - log.debug { "[TimeService.findTimes] 시작" } - - return timeFinder.findAll() - .toResponse() - .also { log.info { "[TimeService.findTimes] 완료. ${it.times.size}개 반환" } } - } - - @Transactional(readOnly = true) - fun findTimesWithAvailability(date: LocalDate, themeId: Long): TimeWithAvailabilityListResponse { - log.debug { "[TimeService.findTimesWithAvailability] 시작: date=$date, themeId=$themeId" } - - val times: List = - timeFinder.findAllWithAvailabilityByDateAndThemeId(date, themeId) - .map { - TimeWithAvailabilityResponse( - id = it.timeId, - startAt = it.startAt, - isAvailable = it.isReservable - ) - } - - return TimeWithAvailabilityListResponse(times) - .also { log.info { "[TimeService.findTimesWithAvailability] ${it.times.size}개 반환: date=$date, themeId=$themeId" } } - } - - @Transactional - fun createTime(request: TimeCreateRequest): TimeCreateResponse { - val startAt: LocalTime = request.startAt - log.debug { "[TimeService.createTime] 시작: startAt=${startAt}" } - - return timeWriter.create(startAt) - .toCreateResponse() - .also { log.info { "[TimeService.createTime] 완료: startAt=${startAt}, timeId=${it.id}" } } - } - - @Transactional - fun deleteTime(id: Long) { - log.debug { "[TimeService.deleteTime] 시작: timeId=$id" } - - val time: TimeEntity = timeFinder.findById(id) - - timeWriter.delete(time) - .also { log.info { "[TimeService.deleteTime] 완료: timeId=$id, startAt=${time.startAt}" } } - } -} diff --git a/src/main/kotlin/roomescape/time/business/domain/TimeWithAvailability.kt b/src/main/kotlin/roomescape/time/business/domain/TimeWithAvailability.kt deleted file mode 100644 index c53000d5..00000000 --- a/src/main/kotlin/roomescape/time/business/domain/TimeWithAvailability.kt +++ /dev/null @@ -1,12 +0,0 @@ -package roomescape.time.business.domain - -import java.time.LocalDate -import java.time.LocalTime - -class TimeWithAvailability( - val timeId: Long, - val startAt: LocalTime, - val date: LocalDate, - val themeId: Long, - val isReservable: Boolean -) diff --git a/src/main/kotlin/roomescape/time/docs/TimeAPI.kt b/src/main/kotlin/roomescape/time/docs/TimeAPI.kt deleted file mode 100644 index 0d0da336..00000000 --- a/src/main/kotlin/roomescape/time/docs/TimeAPI.kt +++ /dev/null @@ -1,50 +0,0 @@ -package roomescape.time.docs - -import io.swagger.v3.oas.annotations.Operation -import io.swagger.v3.oas.annotations.responses.ApiResponse -import io.swagger.v3.oas.annotations.responses.ApiResponses -import io.swagger.v3.oas.annotations.tags.Tag -import jakarta.validation.Valid -import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestParam -import roomescape.auth.web.support.Admin -import roomescape.auth.web.support.LoginRequired -import roomescape.common.dto.response.CommonApiResponse -import roomescape.time.web.TimeCreateRequest -import roomescape.time.web.TimeCreateResponse -import roomescape.time.web.TimeRetrieveListResponse -import roomescape.time.web.TimeWithAvailabilityListResponse -import java.time.LocalDate - -@Tag(name = "4. 예약 시간 API", description = "예약 시간을 조회 / 추가 / 삭제할 때 사용합니다.") -interface TimeAPI { - - @Admin - @Operation(summary = "모든 시간 조회", tags = ["관리자 로그인이 필요한 API"]) - @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) - fun findTimes(): ResponseEntity> - - @Admin - @Operation(summary = "시간 추가", tags = ["관리자 로그인이 필요한 API"]) - @ApiResponses(ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true)) - fun createTime( - @Valid @RequestBody timeCreateRequest: TimeCreateRequest, - ): ResponseEntity> - - @Admin - @Operation(summary = "시간 삭제", tags = ["관리자 로그인이 필요한 API"]) - @ApiResponses(ApiResponse(responseCode = "204", description = "성공", useReturnTypeSchema = true)) - fun deleteTime( - @PathVariable id: Long - ): ResponseEntity> - - @LoginRequired - @Operation(summary = "예약 가능 여부를 포함한 모든 시간 조회", tags = ["로그인이 필요한 API"]) - @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) - fun findTimesWithAvailability( - @RequestParam date: LocalDate, - @RequestParam themeId: Long - ): ResponseEntity> -} \ No newline at end of file diff --git a/src/main/kotlin/roomescape/time/exception/TimeErrorCode.kt b/src/main/kotlin/roomescape/time/exception/TimeErrorCode.kt deleted file mode 100644 index d115c326..00000000 --- a/src/main/kotlin/roomescape/time/exception/TimeErrorCode.kt +++ /dev/null @@ -1,14 +0,0 @@ -package roomescape.time.exception - -import org.springframework.http.HttpStatus -import roomescape.common.exception.ErrorCode - -enum class TimeErrorCode( - override val httpStatus: HttpStatus, - override val errorCode: String, - override val message: String -) : ErrorCode { - TIME_NOT_FOUND(HttpStatus.NOT_FOUND, "TM001", "시간을 찾을 수 없어요."), - TIME_DUPLICATED(HttpStatus.BAD_REQUEST, "TM002", "이미 같은 시간이 있어요."), - TIME_ALREADY_RESERVED(HttpStatus.CONFLICT, "TM003", "예약된 시간이라 삭제할 수 없어요.") -} diff --git a/src/main/kotlin/roomescape/time/exception/TimeException.kt b/src/main/kotlin/roomescape/time/exception/TimeException.kt deleted file mode 100644 index 75b4a698..00000000 --- a/src/main/kotlin/roomescape/time/exception/TimeException.kt +++ /dev/null @@ -1,9 +0,0 @@ -package roomescape.time.exception - -import roomescape.common.exception.ErrorCode -import roomescape.common.exception.RoomescapeException - -class TimeException( - override val errorCode: ErrorCode, - override val message: String = errorCode.message -) : RoomescapeException(errorCode, message) diff --git a/src/main/kotlin/roomescape/time/implement/TimeFinder.kt b/src/main/kotlin/roomescape/time/implement/TimeFinder.kt deleted file mode 100644 index 60911a89..00000000 --- a/src/main/kotlin/roomescape/time/implement/TimeFinder.kt +++ /dev/null @@ -1,60 +0,0 @@ -package roomescape.time.implement - -import io.github.oshai.kotlinlogging.KLogger -import io.github.oshai.kotlinlogging.KotlinLogging -import org.springframework.data.repository.findByIdOrNull -import org.springframework.stereotype.Component -import roomescape.reservation.implement.ReservationFinder -import roomescape.reservation.infrastructure.persistence.ReservationEntity -import roomescape.time.business.domain.TimeWithAvailability -import roomescape.theme.implement.ThemeFinder -import roomescape.time.exception.TimeErrorCode -import roomescape.time.exception.TimeException -import roomescape.time.infrastructure.persistence.TimeEntity -import roomescape.time.infrastructure.persistence.TimeRepository -import java.time.LocalDate - -private val log: KLogger = KotlinLogging.logger {} - -@Component -class TimeFinder( - private val timeRepository: TimeRepository, - private val reservationFinder: ReservationFinder, - private val themeFinder: ThemeFinder -) { - - fun findAll(): List { - log.debug { "[TimeFinder.findAll] 시작" } - - return timeRepository.findAll() - .also { log.debug { "[TimeFinder.findAll] ${it.size}개 시간 조회 완료" } } - } - - fun findById(id: Long): TimeEntity { - log.debug { "[TimeFinder.findById] 조회 시작: timeId=$id" } - - return timeRepository.findByIdOrNull(id) - ?.also { log.debug { "[TimeFinder.findById] 조회 완료: timeId=$id, startAt=${it.startAt}" } } - ?: run { - log.warn { "[TimeFinder.findById] 조회 실패: timeId=$id" } - throw TimeException(TimeErrorCode.TIME_NOT_FOUND) - } - } - - fun findAllWithAvailabilityByDateAndThemeId( - date: LocalDate, themeId: Long - ): List { - log.debug { "[TimeFinder.findAllWithAvailabilityByDateAndThemeId] 조회 시작: date:$date, themeId=$themeId" } - - val theme = themeFinder.findById(themeId) - val reservations: List = reservationFinder.findAllByDateAndTheme(date, theme) - val allTimes: List = findAll() - - return allTimes.map { time -> - val isReservable: Boolean = reservations.none { reservation -> time.id == reservation.time.id } - TimeWithAvailability(time.id!!, time.startAt, date, themeId, isReservable) - }.also { - log.debug { "[TimeFinder.findAllWithAvailabilityByDateAndThemeId] ${it.size}개 조회 완료: date:$date, themeId=$themeId" } - } - } -} diff --git a/src/main/kotlin/roomescape/time/implement/TimeValidator.kt b/src/main/kotlin/roomescape/time/implement/TimeValidator.kt deleted file mode 100644 index ef0cee93..00000000 --- a/src/main/kotlin/roomescape/time/implement/TimeValidator.kt +++ /dev/null @@ -1,41 +0,0 @@ -package roomescape.time.implement - -import io.github.oshai.kotlinlogging.KLogger -import io.github.oshai.kotlinlogging.KotlinLogging -import org.springframework.stereotype.Component -import roomescape.reservation.implement.ReservationFinder -import roomescape.time.exception.TimeErrorCode -import roomescape.time.exception.TimeException -import roomescape.time.infrastructure.persistence.TimeEntity -import roomescape.time.infrastructure.persistence.TimeRepository -import java.time.LocalTime - -private val log: KLogger = KotlinLogging.logger {} - -@Component -class TimeValidator( - private val timeRepository: TimeRepository, - private val reservationFinder: ReservationFinder -) { - fun validateIsAlreadyExists(startAt: LocalTime) { - log.debug { "[TimeValidator.validateIsAlreadyExists] 시작: startAt=${startAt}" } - - if (timeRepository.existsByStartAt(startAt)) { - log.info { "[TimeValidator.validateIsAlreadyExists] 중복 시간: startAt=$startAt" } - throw TimeException(TimeErrorCode.TIME_DUPLICATED) - } - - log.debug { "[TimeValidator.validateIsAlreadyExists] 완료: startAt=${startAt}" } - } - - fun validateIsReserved(time: TimeEntity) { - log.debug { "[TimeValidator.validateIsReserved] 시작: id=${time.id}, startAt=${time.startAt}" } - - if (reservationFinder.isTimeReserved(time)) { - log.info { "[TimeValidator.validateIsReserved] 예약이 있는 시간: timeId=${time.id}, startAt=${time.startAt}" } - throw TimeException(TimeErrorCode.TIME_ALREADY_RESERVED) - } - - log.debug { "[TimeValidator.validateIsReserved] 시작: id=${time.id}, startAt=${time.startAt}" } - } -} diff --git a/src/main/kotlin/roomescape/time/implement/TimeWriter.kt b/src/main/kotlin/roomescape/time/implement/TimeWriter.kt deleted file mode 100644 index f1013697..00000000 --- a/src/main/kotlin/roomescape/time/implement/TimeWriter.kt +++ /dev/null @@ -1,37 +0,0 @@ -package roomescape.time.implement - -import com.github.f4b6a3.tsid.TsidFactory -import io.github.oshai.kotlinlogging.KLogger -import io.github.oshai.kotlinlogging.KotlinLogging -import org.springframework.stereotype.Component -import roomescape.common.config.next -import roomescape.time.infrastructure.persistence.TimeEntity -import roomescape.time.infrastructure.persistence.TimeRepository -import java.time.LocalTime - -private val log: KLogger = KotlinLogging.logger {} - -@Component -class TimeWriter( - private val timeValidator: TimeValidator, - private val timeRepository: TimeRepository, - private val tsidFactory: TsidFactory -) { - fun create(startAt: LocalTime): TimeEntity { - log.debug { "[TimeWriter.create] 시작: startAt=$startAt" } - timeValidator.validateIsAlreadyExists(startAt) - - val time = TimeEntity(_id = tsidFactory.next(), startAt = startAt) - - return timeRepository.save(time) - .also { log.debug { "[TimeWriter.create] 완료: startAt=$startAt, id=${it.id}" } } - } - - fun delete(time: TimeEntity) { - log.debug { "[TimeWriter.delete] 시작: id=${time.id}" } - timeValidator.validateIsReserved(time) - - timeRepository.delete(time) - .also { log.debug { "[TimeWriter.delete] 완료: id=${time.id}, startAt=${time.startAt}" } } - } -} diff --git a/src/main/kotlin/roomescape/time/infrastructure/persistence/TimeEntity.kt b/src/main/kotlin/roomescape/time/infrastructure/persistence/TimeEntity.kt deleted file mode 100644 index 3e31dd80..00000000 --- a/src/main/kotlin/roomescape/time/infrastructure/persistence/TimeEntity.kt +++ /dev/null @@ -1,21 +0,0 @@ -package roomescape.time.infrastructure.persistence - -import jakarta.persistence.Column -import jakarta.persistence.Entity -import jakarta.persistence.Id -import jakarta.persistence.Table -import roomescape.common.entity.BaseEntity -import java.time.LocalTime - -@Entity -@Table(name = "times") -class TimeEntity( - @Id - @Column(name = "time_id") - private var _id: Long?, - - @Column(name = "start_at", nullable = false) - var startAt: LocalTime, -): BaseEntity() { - override fun getId(): Long? = _id -} diff --git a/src/main/kotlin/roomescape/time/infrastructure/persistence/TimeRepository.kt b/src/main/kotlin/roomescape/time/infrastructure/persistence/TimeRepository.kt deleted file mode 100644 index 5e726669..00000000 --- a/src/main/kotlin/roomescape/time/infrastructure/persistence/TimeRepository.kt +++ /dev/null @@ -1,8 +0,0 @@ -package roomescape.time.infrastructure.persistence - -import org.springframework.data.jpa.repository.JpaRepository -import java.time.LocalTime - -interface TimeRepository : JpaRepository { - fun existsByStartAt(startAt: LocalTime): Boolean -} diff --git a/src/main/kotlin/roomescape/time/web/TimeController.kt b/src/main/kotlin/roomescape/time/web/TimeController.kt deleted file mode 100644 index ad079568..00000000 --- a/src/main/kotlin/roomescape/time/web/TimeController.kt +++ /dev/null @@ -1,51 +0,0 @@ -package roomescape.time.web - -import jakarta.validation.Valid -import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.* -import roomescape.common.dto.response.CommonApiResponse -import roomescape.time.business.TimeService -import roomescape.time.docs.TimeAPI -import java.net.URI -import java.time.LocalDate - -@RestController -class TimeController( - private val timeService: TimeService -) : TimeAPI { - - @GetMapping("/times") - override fun findTimes(): ResponseEntity> { - val response: TimeRetrieveListResponse = timeService.findTimes() - - return ResponseEntity.ok(CommonApiResponse(response)) - } - - @PostMapping("/times") - override fun createTime( - @Valid @RequestBody timeCreateRequest: TimeCreateRequest, - ): ResponseEntity> { - val response: TimeCreateResponse = timeService.createTime(timeCreateRequest) - - return ResponseEntity - .created(URI.create("/times/${response.id}")) - .body(CommonApiResponse(response)) - } - - @DeleteMapping("/times/{id}") - override fun deleteTime(@PathVariable id: Long): ResponseEntity> { - timeService.deleteTime(id) - - return ResponseEntity.noContent().build() - } - - @GetMapping("/times/search") - override fun findTimesWithAvailability( - @RequestParam date: LocalDate, - @RequestParam themeId: Long - ): ResponseEntity> { - val response: TimeWithAvailabilityListResponse = timeService.findTimesWithAvailability(date, themeId) - - return ResponseEntity.ok(CommonApiResponse(response)) - } -} diff --git a/src/main/kotlin/roomescape/time/web/TimeDTO.kt b/src/main/kotlin/roomescape/time/web/TimeDTO.kt deleted file mode 100644 index 62ee965f..00000000 --- a/src/main/kotlin/roomescape/time/web/TimeDTO.kt +++ /dev/null @@ -1,55 +0,0 @@ -package roomescape.time.web - -import io.swagger.v3.oas.annotations.media.Schema -import roomescape.time.infrastructure.persistence.TimeEntity -import java.time.LocalTime - -@Schema(name = "예약 시간 저장 요청", description = "예약 시간 저장 요청시 사용됩니다.") -data class TimeCreateRequest( - @Schema(description = "시간", type = "string", example = "09:00") - val startAt: LocalTime -) - -@Schema(name = "예약 시간 정보", description = "예약 시간 추가 및 조회 응답시 사용됩니다.") -data class TimeCreateResponse( - @Schema(description = "시간 식별자") - val id: Long, - - @Schema(description = "시간") - val startAt: LocalTime -) - -fun TimeEntity.toCreateResponse(): TimeCreateResponse = TimeCreateResponse(this.id!!, this.startAt) - -data class TimeRetrieveResponse( - @Schema(description = "시간 식별자.") - val id: Long, - - @Schema(description = "시간") - val startAt: LocalTime -) - -fun TimeEntity.toResponse(): TimeRetrieveResponse = TimeRetrieveResponse(this.id!!, this.startAt) - -data class TimeRetrieveListResponse( - val times: List -) - -fun List.toResponse(): TimeRetrieveListResponse = TimeRetrieveListResponse( - this.map { it.toResponse() } -) - -data class TimeWithAvailabilityResponse( - @Schema(description = "시간 식별자") - val id: Long, - - @Schema(description = "시간") - val startAt: LocalTime, - - @Schema(description = "예약 가능 여부") - val isAvailable: Boolean -) - -data class TimeWithAvailabilityListResponse( - val times: List -) diff --git a/src/test/kotlin/roomescape/time/business/TimeServiceTest.kt b/src/test/kotlin/roomescape/time/business/TimeServiceTest.kt deleted file mode 100644 index 95add3b7..00000000 --- a/src/test/kotlin/roomescape/time/business/TimeServiceTest.kt +++ /dev/null @@ -1,180 +0,0 @@ -package roomescape.time.business - -import io.kotest.assertions.assertSoftly -import io.kotest.assertions.throwables.shouldNotThrow -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.collections.shouldContainExactly -import io.kotest.matchers.collections.shouldHaveSize -import io.kotest.matchers.shouldBe -import io.mockk.Runs -import io.mockk.every -import io.mockk.just -import io.mockk.mockk -import roomescape.time.business.domain.TimeWithAvailability -import roomescape.theme.exception.ThemeErrorCode -import roomescape.theme.exception.ThemeException -import roomescape.time.exception.TimeErrorCode -import roomescape.time.exception.TimeException -import roomescape.time.implement.TimeFinder -import roomescape.time.implement.TimeWriter -import roomescape.time.infrastructure.persistence.TimeEntity -import roomescape.time.web.TimeCreateRequest -import roomescape.util.TimeFixture -import java.time.LocalDate -import java.time.LocalTime - -class TimeServiceTest : FunSpec({ - val timeFinder: TimeFinder = mockk() - val timeWriter: TimeWriter = mockk() - - val timeService = TimeService(timeFinder, timeWriter) - - context("findById") { - val id = 1L - - test("정상 응답") { - every { - timeFinder.findById(id) - } returns TimeFixture.create(id = id) - - timeService.findById(id).id shouldBe id - } - - test("시간을 찾을 수 없으면 예외 응답") { - every { - timeFinder.findById(id) - } throws TimeException(TimeErrorCode.TIME_NOT_FOUND) - - shouldThrow { - timeService.findById(id) - }.also { - it.errorCode shouldBe TimeErrorCode.TIME_NOT_FOUND - } - } - } - - context("findTimes") { - test("정상 응답") { - val times: List = listOf( - TimeFixture.create(startAt = LocalTime.now()), - TimeFixture.create(startAt = LocalTime.now().plusMinutes(1)), - TimeFixture.create(startAt = LocalTime.now().plusMinutes(2)) - ) - - every { - timeFinder.findAll() - } returns times - - val response = timeService.findTimes() - - assertSoftly(response.times) { - it shouldHaveSize times.size - it.map { time -> time.startAt } shouldContainExactly times.map { time -> time.startAt } - } - } - } - - context("findTimesWithAvailability") { - val date = LocalDate.now() - val themeId = 1L - - test("정상 응답") { - val times: List = listOf( - TimeWithAvailability(1, LocalTime.now(), date, themeId, true), - TimeWithAvailability(2, LocalTime.now().plusMinutes(1), date, themeId, false), - TimeWithAvailability(3, LocalTime.now().plusMinutes(2), date, themeId, true) - ) - - every { - timeFinder.findAllWithAvailabilityByDateAndThemeId(date, themeId) - } returns times - - val response = timeService.findTimesWithAvailability(date, themeId) - - assertSoftly(response.times) { - it shouldHaveSize times.size - it.map { time -> time.isAvailable } shouldContainExactly times.map { time -> time.isReservable } - } - } - - test("테마를 찾을 수 없으면 예외 응답") { - every { - timeFinder.findAllWithAvailabilityByDateAndThemeId(date, themeId) - } throws ThemeException(ThemeErrorCode.THEME_NOT_FOUND) - - shouldThrow { - timeService.findTimesWithAvailability(date, themeId) - }.also { - it.errorCode shouldBe ThemeErrorCode.THEME_NOT_FOUND - } - } - } - - context("createTime") { - val request = TimeCreateRequest(startAt = LocalTime.of(10, 0)) - - test("정상 응답") { - val time: TimeEntity = TimeFixture.create(startAt = request.startAt) - - every { - timeWriter.create(request.startAt) - } returns time - - val response = timeService.createTime(request) - response.id shouldBe time.id - } - - test("중복된 시간이 있으면 예외 응답") { - every { - timeWriter.create(request.startAt) - } throws TimeException(TimeErrorCode.TIME_DUPLICATED) - - shouldThrow { - timeService.createTime(request) - }.also { - it.errorCode shouldBe TimeErrorCode.TIME_DUPLICATED - } - } - } - - context("deleteTime") { - test("정상 응답") { - val id = 1L - val time = TimeFixture.create(id = id) - - every { timeFinder.findById(id) } returns time - every { timeWriter.delete(time) } just Runs - - shouldNotThrow { - timeService.deleteTime(id) - } - } - - test("시간을 찾을 수 없으면 예외 응답") { - val id = 1L - - every { timeFinder.findById(id) } throws TimeException(TimeErrorCode.TIME_NOT_FOUND) - - shouldThrow { - timeService.deleteTime(id) - }.also { - it.errorCode shouldBe TimeErrorCode.TIME_NOT_FOUND - } - } - - test("예약이 있는 시간이면 예외 응답") { - val id = 1L - val time = TimeFixture.create() - - every { timeFinder.findById(id) } returns time - every { timeWriter.delete(time) } throws TimeException(TimeErrorCode.TIME_ALREADY_RESERVED) - - shouldThrow { - timeService.deleteTime(id) - }.also { - it.errorCode shouldBe TimeErrorCode.TIME_ALREADY_RESERVED - } - } - } -}) diff --git a/src/test/kotlin/roomescape/time/implement/TimeFinderTest.kt b/src/test/kotlin/roomescape/time/implement/TimeFinderTest.kt deleted file mode 100644 index ba6014a9..00000000 --- a/src/test/kotlin/roomescape/time/implement/TimeFinderTest.kt +++ /dev/null @@ -1,144 +0,0 @@ -package roomescape.time.implement - -import io.kotest.assertions.assertSoftly -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.collections.shouldHaveSize -import io.kotest.matchers.shouldBe -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import org.springframework.data.repository.findByIdOrNull -import roomescape.reservation.implement.ReservationFinder -import roomescape.reservation.infrastructure.persistence.ReservationEntity -import roomescape.time.business.domain.TimeWithAvailability -import roomescape.theme.exception.ThemeErrorCode -import roomescape.theme.exception.ThemeException -import roomescape.theme.implement.ThemeFinder -import roomescape.time.exception.TimeErrorCode -import roomescape.time.exception.TimeException -import roomescape.time.infrastructure.persistence.TimeRepository -import roomescape.util.TimeFixture -import java.time.LocalDate -import java.time.LocalTime - -class TimeFinderTest : FunSpec({ - val timeRepository: TimeRepository = mockk() - val reservationFinder: ReservationFinder = mockk() - val themeFinder: ThemeFinder = mockk() - - val timeFinder = TimeFinder(timeRepository, reservationFinder, themeFinder) - - context("findAll") { - test("모든 시간을 조회한다.") { - every { - timeRepository.findAll() - } returns listOf(mockk(), mockk(), mockk()) - - timeFinder.findAll() shouldHaveSize 3 - } - } - - context("findById") { - val timeId = 1L - test("동일한 ID인 시간을 찾아 응답한다.") { - every { - timeRepository.findByIdOrNull(timeId) - } returns mockk() - - timeFinder.findById(timeId) - - verify(exactly = 1) { - timeRepository.findByIdOrNull(timeId) - } - } - - test("동일한 ID인 시간이 없으면 실패한다.") { - every { - timeRepository.findByIdOrNull(timeId) - } returns null - - shouldThrow { - timeFinder.findById(timeId) - }.also { - it.errorCode shouldBe TimeErrorCode.TIME_NOT_FOUND - } - } - } - - context("findAllWithAvailabilityByDateAndThemeId") { - val date = LocalDate.now() - val themeId = 1L - - test("테마를 찾을 수 없으면 실패한다.") { - every { - themeFinder.findById(themeId) - } throws ThemeException(ThemeErrorCode.THEME_NOT_FOUND) - - shouldThrow { - timeFinder.findAllWithAvailabilityByDateAndThemeId(date, themeId) - }.also { - it.errorCode shouldBe ThemeErrorCode.THEME_NOT_FOUND - } - } - - test("날짜, 테마에 맞는 예약 자체가 없으면 모든 시간이 예약 가능하다.") { - every { - themeFinder.findById(themeId) - } returns mockk() - - every { - reservationFinder.findAllByDateAndTheme(date, any()) - } returns emptyList() - - every { - timeRepository.findAll() - } returns listOf( - TimeFixture.create(startAt = LocalTime.now()), - TimeFixture.create(startAt = LocalTime.now().plusMinutes(30)) - ) - - val result: List = - timeFinder.findAllWithAvailabilityByDateAndThemeId(date, themeId) - - assertSoftly(result) { - it shouldHaveSize 2 - it.all { time -> time.isReservable } - } - } - - test("날짜, 테마에 맞는 예약이 있으면 예약할 수 없다.") { - val times = listOf( - TimeFixture.create(startAt = LocalTime.now()), - TimeFixture.create(startAt = LocalTime.now().plusMinutes(30)) - ) - every { - themeFinder.findById(themeId) - } returns mockk() - - every { - timeRepository.findAll() - } returns times - - every { - reservationFinder.findAllByDateAndTheme(date, any()) - } returns listOf( - mockk().apply { - every { time.id } returns times[0].id - }, - mockk().apply { - every { time.id } returns 0 - } - ) - - val result: List = - timeFinder.findAllWithAvailabilityByDateAndThemeId(date, themeId) - - assertSoftly(result) { - it shouldHaveSize 2 - it[0].isReservable shouldBe false - it[1].isReservable shouldBe true - } - } - } -}) diff --git a/src/test/kotlin/roomescape/time/implement/TimeValidatorTest.kt b/src/test/kotlin/roomescape/time/implement/TimeValidatorTest.kt deleted file mode 100644 index ff835923..00000000 --- a/src/test/kotlin/roomescape/time/implement/TimeValidatorTest.kt +++ /dev/null @@ -1,74 +0,0 @@ -package roomescape.time.implement - -import io.kotest.assertions.throwables.shouldNotThrow -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import io.mockk.every -import io.mockk.mockk -import roomescape.reservation.implement.ReservationFinder -import roomescape.time.exception.TimeErrorCode -import roomescape.time.exception.TimeException -import roomescape.time.infrastructure.persistence.TimeEntity -import roomescape.time.infrastructure.persistence.TimeRepository -import roomescape.util.TimeFixture -import java.time.LocalTime - -class TimeValidatorTest : FunSpec({ - val timeRepository: TimeRepository = mockk() - val reservationFinder: ReservationFinder = mockk() - - val timeValidator = TimeValidator(timeRepository, reservationFinder) - - context("validateIsAlreadyExists") { - val startAt = LocalTime.now() - - test("같은 이메일을 가진 회원이 있으면 예외를 던진다.") { - every { - timeRepository.existsByStartAt(startAt) - } returns true - - shouldThrow { - timeValidator.validateIsAlreadyExists(startAt) - }.also { - it.errorCode shouldBe TimeErrorCode.TIME_DUPLICATED - } - } - - test("같은 이메일을 가진 회원이 없으면 종료한다.") { - every { - timeRepository.existsByStartAt(startAt) - } returns false - - shouldNotThrow { - timeValidator.validateIsAlreadyExists(startAt) - } - } - } - - context("validateIsReserved") { - val time: TimeEntity = TimeFixture.create(startAt = LocalTime.now()) - - test("해당 시간에 예약이 있으면 예외를 던진다.") { - every { - reservationFinder.isTimeReserved(time) - } returns true - - shouldThrow { - timeValidator.validateIsReserved(time) - }.also { - it.errorCode shouldBe TimeErrorCode.TIME_ALREADY_RESERVED - } - } - - test("해당 시간에 예약이 없으면 종료한다.") { - every { - reservationFinder.isTimeReserved(time) - } returns false - - shouldNotThrow { - timeValidator.validateIsReserved(time) - } - } - } -}) diff --git a/src/test/kotlin/roomescape/time/implement/TimeWriterTest.kt b/src/test/kotlin/roomescape/time/implement/TimeWriterTest.kt deleted file mode 100644 index 017e7047..00000000 --- a/src/test/kotlin/roomescape/time/implement/TimeWriterTest.kt +++ /dev/null @@ -1,84 +0,0 @@ -package roomescape.time.implement - -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import io.mockk.* -import roomescape.time.exception.TimeErrorCode -import roomescape.time.exception.TimeException -import roomescape.time.infrastructure.persistence.TimeEntity -import roomescape.time.infrastructure.persistence.TimeRepository -import roomescape.util.TimeFixture -import roomescape.util.TsidFactory -import java.time.LocalTime - -class TimeWriterTest : FunSpec({ - - val timeValidator: TimeValidator = mockk() - val timeRepository: TimeRepository = mockk() - - val timeWriter = TimeWriter(timeValidator, timeRepository, TsidFactory) - - context("create") { - val startAt = LocalTime.now() - - test("중복된 시간이 있으면 실패한다.") { - every { - timeValidator.validateIsAlreadyExists(startAt) - } throws TimeException(TimeErrorCode.TIME_DUPLICATED) - - shouldThrow { - timeWriter.create(startAt) - }.also { - it.errorCode shouldBe TimeErrorCode.TIME_DUPLICATED - } - } - - test("중복된 시간이 없으면 저장한다.") { - every { - timeValidator.validateIsAlreadyExists(startAt) - } just Runs - - every { - timeRepository.save(any()) - } returns TimeFixture.create(startAt = startAt) - - timeWriter.create(startAt) - - verify(exactly = 1) { - timeRepository.save(any()) - } - } - } - - context("delete") { - val time: TimeEntity = TimeFixture.create() - test("예약이 있는 시간이면 실패한다.") { - every { - timeValidator.validateIsReserved(time) - } throws TimeException(TimeErrorCode.TIME_ALREADY_RESERVED) - - shouldThrow { - timeWriter.delete(time) - }.also { - it.errorCode shouldBe TimeErrorCode.TIME_ALREADY_RESERVED - } - } - - test("예약이 없는 시간이면 제거한다.") { - every { - timeValidator.validateIsReserved(time) - } just Runs - - every { - timeRepository.delete(time) - } just Runs - - timeWriter.delete(time) - - verify(exactly = 1) { - timeRepository.delete(time) - } - } - } -}) diff --git a/src/test/kotlin/roomescape/time/infrastructure/persistence/TimeRepositoryTest.kt b/src/test/kotlin/roomescape/time/infrastructure/persistence/TimeRepositoryTest.kt deleted file mode 100644 index bc79357b..00000000 --- a/src/test/kotlin/roomescape/time/infrastructure/persistence/TimeRepositoryTest.kt +++ /dev/null @@ -1,33 +0,0 @@ -package roomescape.time.infrastructure.persistence - -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import jakarta.persistence.EntityManager -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import roomescape.util.TimeFixture -import java.time.LocalTime - -@DataJpaTest(showSql = false) -class TimeRepositoryTest( - val entityManager: EntityManager, - val timeRepository: TimeRepository, -) : FunSpec({ - - context("existsByStartAt") { - val startAt = LocalTime.of(10, 0) - - beforeTest { - entityManager.persist(TimeFixture.create(startAt = startAt)) - entityManager.flush() - entityManager.clear() - } - - test("동일한 시간이 있으면 true 반환") { - timeRepository.existsByStartAt(startAt) shouldBe true - } - - test("동일한 시간이 없으면 false 반환") { - timeRepository.existsByStartAt(startAt.plusSeconds(1)) shouldBe false - } - } -}) \ No newline at end of file diff --git a/src/test/kotlin/roomescape/time/web/TimeControllerTest.kt b/src/test/kotlin/roomescape/time/web/TimeControllerTest.kt deleted file mode 100644 index 42543d34..00000000 --- a/src/test/kotlin/roomescape/time/web/TimeControllerTest.kt +++ /dev/null @@ -1,312 +0,0 @@ -package roomescape.time.web - -import com.ninjasquad.springmockk.MockkBean -import io.kotest.assertions.assertSoftly -import io.kotest.matchers.collections.shouldHaveSize -import io.kotest.matchers.shouldBe -import io.mockk.every -import org.hamcrest.Matchers.equalTo -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest -import org.springframework.http.MediaType -import org.springframework.test.web.servlet.MockMvc -import roomescape.auth.exception.AuthErrorCode -import roomescape.theme.exception.ThemeErrorCode -import roomescape.theme.exception.ThemeException -import roomescape.time.business.TimeService -import roomescape.time.exception.TimeErrorCode -import roomescape.time.exception.TimeException -import roomescape.util.RoomescapeApiTest -import roomescape.util.TimeFixture -import java.time.LocalDate -import java.time.LocalTime - -@WebMvcTest(TimeController::class) -class TimeControllerTest(val mockMvc: MockMvc) : RoomescapeApiTest() { - @MockkBean - private lateinit var timeService: TimeService - - init { - Given("GET /times 요청을") { - val endpoint = "/times" - - When("관리자가 보내면") { - beforeTest { - loginAsAdmin() - } - - Then("정상 응답") { - every { - timeService.findTimes() - } returns listOf( - TimeFixture.create(id = 1L), - TimeFixture.create(id = 2L) - ).toResponse() - - runGetTest( - mockMvc = mockMvc, - endpoint = endpoint, - ) { - status { isOk() } - content { - contentType(MediaType.APPLICATION_JSON) - jsonPath("$.data.times[0].id") { value(1) } - jsonPath("$.data.times[1].id") { value(2) } - } - } - } - } - - When("관리자가 보내지 않았다면") { - loginAsUser() - val expectedError = AuthErrorCode.ACCESS_DENIED - - Then("예외 응답") { - runGetTest( - mockMvc = mockMvc, - endpoint = endpoint, - ) { - status { isEqualTo(expectedError.httpStatus.value()) } - }.andExpect { - content { - contentType(MediaType.APPLICATION_JSON) - jsonPath("$.code") { equalTo(expectedError.errorCode) } - } - } - } - } - } - - Given("POST /times 요청을") { - val endpoint = "/times" - - When("관리자가 보낼 때") { - beforeTest { - loginAsAdmin() - } - val time = LocalTime.of(10, 0) - val request = TimeCreateRequest(startAt = time) - - Then("시간 형식이 HH:mm이 아니거나, 범위를 벗어나면 400 응답") { - listOf( - "{\"startAt\": \"23:30:30\"}", - "{\"startAt\": \"24:59\"}", - ).forEach { - runPostTest( - mockMvc = mockMvc, - endpoint = endpoint, - body = it, - ) { - status { isBadRequest() } - } - } - } - - Then("정상 응답") { - every { - timeService.createTime(request) - } returns TimeCreateResponse(id = 1, startAt = time) - - runPostTest( - mockMvc = mockMvc, - endpoint = endpoint, - body = request, - ) { - status { isCreated() } - content { - contentType(MediaType.APPLICATION_JSON) - jsonPath("$.data.id") { value(1) } - jsonPath("$.data.startAt") { value("10:00") } - } - } - } - - Then("동일한 시간이 존재하면 예외 응답") { - val expectedError = TimeErrorCode.TIME_DUPLICATED - every { - timeService.createTime(request) - } throws TimeException(expectedError) - - runPostTest( - mockMvc = mockMvc, - endpoint = endpoint, - body = request, - ) { - status { isEqualTo(expectedError.httpStatus.value()) } - content { - contentType(MediaType.APPLICATION_JSON) - jsonPath("$.code") { equalTo(expectedError.errorCode) } - } - } - } - } - - When("관리자가 보내지 않았다면") { - loginAsUser() - - Then("예외 응답") { - val expectedError = AuthErrorCode.ACCESS_DENIED - runPostTest( - mockMvc = mockMvc, - endpoint = endpoint, - body = TimeFixture.create(), - ) { - status { isEqualTo(expectedError.httpStatus.value()) } - jsonPath("$.code", equalTo(expectedError.errorCode)) - } - } - } - } - - Given("DELETE /times/{id} 요청을") { - val endpoint = "/times/1" - - When("관리자가 보낼 때") { - beforeTest { - loginAsAdmin() - } - - Then("정상 응답") { - every { - timeService.deleteTime(1L) - } returns Unit - - runDeleteTest( - mockMvc = mockMvc, - endpoint = endpoint, - ) { - status { isNoContent() } - } - } - - Then("없는 시간을 조회하면 예외 응답") { - val id = 1L - val expectedError = TimeErrorCode.TIME_NOT_FOUND - - every { - timeService.deleteTime(id) - } throws TimeException(expectedError) - - runDeleteTest( - mockMvc = mockMvc, - endpoint = "/times/$id", - ) { - status { isEqualTo(expectedError.httpStatus.value()) } - content { - contentType(MediaType.APPLICATION_JSON) - jsonPath("$.code") { equalTo(expectedError.errorCode) } - } - } - } - - Then("예약이 있는 시간을 삭제하면 예외 응답") { - val id = 1L - val expectedError = TimeErrorCode.TIME_ALREADY_RESERVED - - every { - timeService.deleteTime(id) - } throws TimeException(expectedError) - - runDeleteTest( - mockMvc = mockMvc, - endpoint = "/times/$id", - ) { - status { isEqualTo(expectedError.httpStatus.value()) } - content { - contentType(MediaType.APPLICATION_JSON) - jsonPath("$.code") { equalTo(expectedError.errorCode) } - } - } - } - } - - When("관리자가 보내지 않았다면") { - loginAsUser() - - Then("예외 응답") { - val expectedError = AuthErrorCode.ACCESS_DENIED - runDeleteTest( - mockMvc = mockMvc, - endpoint = endpoint, - ) { - status { isEqualTo(expectedError.httpStatus.value()) } - jsonPath("$.code", equalTo(expectedError.errorCode)) - } - } - } - } - - Given("GET /times/search?date={date}&themeId={themeId} 요청을 ") { - val date: LocalDate = LocalDate.now() - val themeId = 1L - - When("회원이 보낼 때") { - beforeTest { - loginAsUser() - } - - Then("정상 응답") { - val response = TimeWithAvailabilityListResponse( - listOf( - TimeWithAvailabilityResponse(1L, LocalTime.of(10, 0), true), - TimeWithAvailabilityResponse(2L, LocalTime.of(10, 1), false), - TimeWithAvailabilityResponse(3L, LocalTime.of(10, 2), true) - ) - ) - every { - timeService.findTimesWithAvailability(date, themeId) - } returns response - - val result = runGetTest( - mockMvc = mockMvc, - endpoint = "/times/search?date=$date&themeId=$themeId", - ) { - status { isOk() } - content { - contentType(MediaType.APPLICATION_JSON) - } - }.andReturn().readValue(TimeWithAvailabilityListResponse::class.java) - - assertSoftly(result.times) { - this shouldHaveSize response.times.size - this[0].id shouldBe response.times[0].id - this[0].isAvailable shouldBe response.times[0].isAvailable - - this[1].id shouldBe response.times[1].id - this[1].isAvailable shouldBe response.times[1].isAvailable - } - } - Then("테마를 찾을 수 없으면 예외 응답") { - val expectedError = ThemeErrorCode.THEME_NOT_FOUND - every { - timeService.findTimesWithAvailability(date, themeId) - } throws ThemeException(expectedError) - - runGetTest( - mockMvc = mockMvc, - endpoint = "/times/search?date=$date&themeId=$themeId", - ) { - status { isNotFound() } - jsonPath("$.code", equalTo(expectedError.errorCode)) - } - } - } - - When("비회원이 보내면") { - doNotLogin() - - Then("예외 응답") { - val expectedError = AuthErrorCode.MEMBER_NOT_FOUND - - runGetTest( - mockMvc = mockMvc, - endpoint = "/times/search?date=$date&themeId=$themeId", - ) { - status { isEqualTo(expectedError.httpStatus.value()) } - jsonPath("$.code", equalTo(expectedError.errorCode)) - } - - } - } - } - } -}