generated from pricelees/issue-pr-template
[#41] 예약 스키마 재정의 #42
@ -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<TimeWithAvailabilityResponse> =
|
|
||||||
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}" } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
)
|
|
||||||
@ -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<CommonApiResponse<TimeRetrieveListResponse>>
|
|
||||||
|
|
||||||
@Admin
|
|
||||||
@Operation(summary = "시간 추가", tags = ["관리자 로그인이 필요한 API"])
|
|
||||||
@ApiResponses(ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true))
|
|
||||||
fun createTime(
|
|
||||||
@Valid @RequestBody timeCreateRequest: TimeCreateRequest,
|
|
||||||
): ResponseEntity<CommonApiResponse<TimeCreateResponse>>
|
|
||||||
|
|
||||||
@Admin
|
|
||||||
@Operation(summary = "시간 삭제", tags = ["관리자 로그인이 필요한 API"])
|
|
||||||
@ApiResponses(ApiResponse(responseCode = "204", description = "성공", useReturnTypeSchema = true))
|
|
||||||
fun deleteTime(
|
|
||||||
@PathVariable id: Long
|
|
||||||
): ResponseEntity<CommonApiResponse<Unit>>
|
|
||||||
|
|
||||||
@LoginRequired
|
|
||||||
@Operation(summary = "예약 가능 여부를 포함한 모든 시간 조회", tags = ["로그인이 필요한 API"])
|
|
||||||
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
|
|
||||||
fun findTimesWithAvailability(
|
|
||||||
@RequestParam date: LocalDate,
|
|
||||||
@RequestParam themeId: Long
|
|
||||||
): ResponseEntity<CommonApiResponse<TimeWithAvailabilityListResponse>>
|
|
||||||
}
|
|
||||||
@ -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", "예약된 시간이라 삭제할 수 없어요.")
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
@ -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<TimeEntity> {
|
|
||||||
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<TimeWithAvailability> {
|
|
||||||
log.debug { "[TimeFinder.findAllWithAvailabilityByDateAndThemeId] 조회 시작: date:$date, themeId=$themeId" }
|
|
||||||
|
|
||||||
val theme = themeFinder.findById(themeId)
|
|
||||||
val reservations: List<ReservationEntity> = reservationFinder.findAllByDateAndTheme(date, theme)
|
|
||||||
val allTimes: List<TimeEntity> = 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" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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}" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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}" } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
package roomescape.time.infrastructure.persistence
|
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository
|
|
||||||
import java.time.LocalTime
|
|
||||||
|
|
||||||
interface TimeRepository : JpaRepository<TimeEntity, Long> {
|
|
||||||
fun existsByStartAt(startAt: LocalTime): Boolean
|
|
||||||
}
|
|
||||||
@ -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<CommonApiResponse<TimeRetrieveListResponse>> {
|
|
||||||
val response: TimeRetrieveListResponse = timeService.findTimes()
|
|
||||||
|
|
||||||
return ResponseEntity.ok(CommonApiResponse(response))
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/times")
|
|
||||||
override fun createTime(
|
|
||||||
@Valid @RequestBody timeCreateRequest: TimeCreateRequest,
|
|
||||||
): ResponseEntity<CommonApiResponse<TimeCreateResponse>> {
|
|
||||||
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<CommonApiResponse<Unit>> {
|
|
||||||
timeService.deleteTime(id)
|
|
||||||
|
|
||||||
return ResponseEntity.noContent().build()
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/times/search")
|
|
||||||
override fun findTimesWithAvailability(
|
|
||||||
@RequestParam date: LocalDate,
|
|
||||||
@RequestParam themeId: Long
|
|
||||||
): ResponseEntity<CommonApiResponse<TimeWithAvailabilityListResponse>> {
|
|
||||||
val response: TimeWithAvailabilityListResponse = timeService.findTimesWithAvailability(date, themeId)
|
|
||||||
|
|
||||||
return ResponseEntity.ok(CommonApiResponse(response))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<TimeRetrieveResponse>
|
|
||||||
)
|
|
||||||
|
|
||||||
fun List<TimeEntity>.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<TimeWithAvailabilityResponse>
|
|
||||||
)
|
|
||||||
@ -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<TimeException> {
|
|
||||||
timeService.findById(id)
|
|
||||||
}.also {
|
|
||||||
it.errorCode shouldBe TimeErrorCode.TIME_NOT_FOUND
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
context("findTimes") {
|
|
||||||
test("정상 응답") {
|
|
||||||
val times: List<TimeEntity> = 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<TimeWithAvailability> = 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<ThemeException> {
|
|
||||||
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<TimeException> {
|
|
||||||
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<Exception> {
|
|
||||||
timeService.deleteTime(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test("시간을 찾을 수 없으면 예외 응답") {
|
|
||||||
val id = 1L
|
|
||||||
|
|
||||||
every { timeFinder.findById(id) } throws TimeException(TimeErrorCode.TIME_NOT_FOUND)
|
|
||||||
|
|
||||||
shouldThrow<TimeException> {
|
|
||||||
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<TimeException> {
|
|
||||||
timeService.deleteTime(id)
|
|
||||||
}.also {
|
|
||||||
it.errorCode shouldBe TimeErrorCode.TIME_ALREADY_RESERVED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@ -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<TimeException> {
|
|
||||||
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<ThemeException> {
|
|
||||||
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<TimeWithAvailability> =
|
|
||||||
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<ReservationEntity>().apply {
|
|
||||||
every { time.id } returns times[0].id
|
|
||||||
},
|
|
||||||
mockk<ReservationEntity>().apply {
|
|
||||||
every { time.id } returns 0
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
val result: List<TimeWithAvailability> =
|
|
||||||
timeFinder.findAllWithAvailabilityByDateAndThemeId(date, themeId)
|
|
||||||
|
|
||||||
assertSoftly(result) {
|
|
||||||
it shouldHaveSize 2
|
|
||||||
it[0].isReservable shouldBe false
|
|
||||||
it[1].isReservable shouldBe true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@ -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<TimeException> {
|
|
||||||
timeValidator.validateIsAlreadyExists(startAt)
|
|
||||||
}.also {
|
|
||||||
it.errorCode shouldBe TimeErrorCode.TIME_DUPLICATED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test("같은 이메일을 가진 회원이 없으면 종료한다.") {
|
|
||||||
every {
|
|
||||||
timeRepository.existsByStartAt(startAt)
|
|
||||||
} returns false
|
|
||||||
|
|
||||||
shouldNotThrow<TimeException> {
|
|
||||||
timeValidator.validateIsAlreadyExists(startAt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
context("validateIsReserved") {
|
|
||||||
val time: TimeEntity = TimeFixture.create(startAt = LocalTime.now())
|
|
||||||
|
|
||||||
test("해당 시간에 예약이 있으면 예외를 던진다.") {
|
|
||||||
every {
|
|
||||||
reservationFinder.isTimeReserved(time)
|
|
||||||
} returns true
|
|
||||||
|
|
||||||
shouldThrow<TimeException> {
|
|
||||||
timeValidator.validateIsReserved(time)
|
|
||||||
}.also {
|
|
||||||
it.errorCode shouldBe TimeErrorCode.TIME_ALREADY_RESERVED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test("해당 시간에 예약이 없으면 종료한다.") {
|
|
||||||
every {
|
|
||||||
reservationFinder.isTimeReserved(time)
|
|
||||||
} returns false
|
|
||||||
|
|
||||||
shouldNotThrow<TimeException> {
|
|
||||||
timeValidator.validateIsReserved(time)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@ -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<TimeException> {
|
|
||||||
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<TimeException> {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@ -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))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user