From 8a4f71be391c9d11e988d67e87052e2335db82e6 Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 17 Jul 2025 16:37:27 +0000 Subject: [PATCH] =?UTF-8?q?[#13]=20Theme=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=BD=94=ED=8B=80=EB=A6=B0=20=EB=A7=88?= =?UTF-8?q?=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=20(#15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 📝 관련 이슈 및 PR **PR과 관련된 이슈 번호** - #13 ## ✨ 작업 내용 theme 패키지 내 코드 및 테스트 코틀린 전환 ## 🧪 테스트 다른 테스트는 코틀린으로 전환 시 크게 문제가 없었으나, GET /themes/most-reserved-last-week API의 경우 쿼리에 크게 의존하여 mocking을 사용하는 기존 테스트로 처리하기 애매한 부분이 있었음. 따라서, API 테스트는 mocking이 아닌 RestAssured를 이용한 실제 테스트로 진행하였고 \@RequestParam, 날짜 등 실제 비즈니스와 관련된 부분을 위주로 처리하고 쿼리 자체는 Repository 테스트에서 상세하게 검증하였음. ## 📚 참고 자료 및 기타 패키지를 reservation 안에 넣는 것은 고민이 조금 더 필요할 것 같음. 현재는 단일 매장에 대한 서비스지만 매장별로 분리하는 것을 고민중인 만큼 코틀린 마이그레이션이 끝난 이후 생각해볼 예정 Reviewed-on: https://gitea.pricelees.me/pricelees/roomescape-refactored/pulls/15 Co-authored-by: pricelees Co-committed-by: pricelees --- .../auth/web/support/AuthInterceptors.kt | 4 +- .../dto/response/RoomescapeErrorResponse.kt | 9 +- .../reservation/domain/Reservation.java | 12 +- .../dto/response/ReservationResponse.java | 2 +- .../service/ReservationService.java | 6 +- .../roomescape/theme/business/ThemeService.kt | 74 +++++ .../theme/controller/ThemeController.java | 101 ------ .../java/roomescape/theme/docs/ThemeAPI.kt | 51 +++ .../java/roomescape/theme/domain/Theme.java | 65 ---- .../domain/repository/ThemeRepository.java | 34 -- .../roomescape/theme/dto/ThemeRequest.java | 21 -- .../roomescape/theme/dto/ThemeResponse.java | 21 -- .../roomescape/theme/dto/ThemesResponse.java | 11 - .../infrastructure/persistence/ThemeEntity.kt | 15 + .../persistence/ThemeRepository.kt | 32 ++ .../theme/service/ThemeService.java | 82 ----- .../roomescape/theme/web/ThemeController.kt | 51 +++ .../java/roomescape/theme/web/ThemeDTO.kt | 68 ++++ .../payment/business/PaymentServiceTest.java | 10 +- .../controller/ReservationControllerTest.java | 34 +- .../ReservationTimeControllerTest.java | 6 +- .../reservation/domain/ReservationTest.java | 10 +- .../ReservationSearchSpecificationTest.java | 6 +- .../service/ReservationServiceTest.java | 22 +- .../service/ReservationTimeServiceTest.java | 6 +- .../ReservationWithPaymentServiceTest.java | 12 +- .../theme/business/ThemeServiceTest.kt | 103 ++++++ .../theme/controller/ThemeControllerTest.java | 184 ----------- .../persistence/ThemeRepositoryTest.kt | 147 +++++++++ .../theme/service/ThemeServiceTest.java | 164 ---------- .../theme/util/TestThemeCreateUtil.kt | 44 +++ .../theme/web/MostReservedThemeAPITest.kt | 113 +++++++ .../theme/web/ThemeControllerTest.kt | 307 ++++++++++++++++++ src/test/java/roomescape/util/Fixtures.kt | 8 +- .../java/roomescape/util/RoomescapeApiTest.kt | 24 +- 35 files changed, 1095 insertions(+), 764 deletions(-) create mode 100644 src/main/java/roomescape/theme/business/ThemeService.kt delete mode 100644 src/main/java/roomescape/theme/controller/ThemeController.java create mode 100644 src/main/java/roomescape/theme/docs/ThemeAPI.kt delete mode 100644 src/main/java/roomescape/theme/domain/Theme.java delete mode 100644 src/main/java/roomescape/theme/domain/repository/ThemeRepository.java delete mode 100644 src/main/java/roomescape/theme/dto/ThemeRequest.java delete mode 100644 src/main/java/roomescape/theme/dto/ThemeResponse.java delete mode 100644 src/main/java/roomescape/theme/dto/ThemesResponse.java create mode 100644 src/main/java/roomescape/theme/infrastructure/persistence/ThemeEntity.kt create mode 100644 src/main/java/roomescape/theme/infrastructure/persistence/ThemeRepository.kt delete mode 100644 src/main/java/roomescape/theme/service/ThemeService.java create mode 100644 src/main/java/roomescape/theme/web/ThemeController.kt create mode 100644 src/main/java/roomescape/theme/web/ThemeDTO.kt create mode 100644 src/test/java/roomescape/theme/business/ThemeServiceTest.kt delete mode 100644 src/test/java/roomescape/theme/controller/ThemeControllerTest.java create mode 100644 src/test/java/roomescape/theme/infrastructure/persistence/ThemeRepositoryTest.kt delete mode 100644 src/test/java/roomescape/theme/service/ThemeServiceTest.java create mode 100644 src/test/java/roomescape/theme/util/TestThemeCreateUtil.kt create mode 100644 src/test/java/roomescape/theme/web/MostReservedThemeAPITest.kt create mode 100644 src/test/java/roomescape/theme/web/ThemeControllerTest.kt diff --git a/src/main/java/roomescape/auth/web/support/AuthInterceptors.kt b/src/main/java/roomescape/auth/web/support/AuthInterceptors.kt index f63f4645..20b5b53d 100644 --- a/src/main/java/roomescape/auth/web/support/AuthInterceptors.kt +++ b/src/main/java/roomescape/auth/web/support/AuthInterceptors.kt @@ -69,9 +69,9 @@ class AdminInterceptor( val token: String? = request.accessTokenCookie().value val memberId: Long = jwtHandler.getMemberIdFromToken(token) member = memberService.findById(memberId) - } catch (e: RoomescapeException) { + } catch (_: RoomescapeException) { response.sendRedirect("/login") - throw e + throw RoomescapeException(ErrorType.LOGIN_REQUIRED, HttpStatus.FORBIDDEN) } with(member) { diff --git a/src/main/java/roomescape/common/dto/response/RoomescapeErrorResponse.kt b/src/main/java/roomescape/common/dto/response/RoomescapeErrorResponse.kt index 045a33c2..819eb297 100644 --- a/src/main/java/roomescape/common/dto/response/RoomescapeErrorResponse.kt +++ b/src/main/java/roomescape/common/dto/response/RoomescapeErrorResponse.kt @@ -8,11 +8,4 @@ import roomescape.common.exception.ErrorType data class RoomescapeErrorResponse( val errorType: ErrorType, val message: String -) { - - companion object { - @JvmStatic - fun of(errorType: ErrorType, message: String? = null): RoomescapeErrorResponse = - RoomescapeErrorResponse(errorType, message ?: errorType.description) - } -} +) diff --git a/src/main/java/roomescape/reservation/domain/Reservation.java b/src/main/java/roomescape/reservation/domain/Reservation.java index 228f5b04..6509596f 100644 --- a/src/main/java/roomescape/reservation/domain/Reservation.java +++ b/src/main/java/roomescape/reservation/domain/Reservation.java @@ -18,7 +18,7 @@ import jakarta.persistence.ManyToOne; import roomescape.common.exception.ErrorType; import roomescape.common.exception.RoomescapeException; import roomescape.member.infrastructure.persistence.Member; -import roomescape.theme.domain.Theme; +import roomescape.theme.infrastructure.persistence.ThemeEntity; @Entity public class Reservation { @@ -35,7 +35,7 @@ public class Reservation { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "theme_id", nullable = false) - private Theme theme; + private ThemeEntity theme; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id", nullable = false) @@ -50,7 +50,7 @@ public class Reservation { public Reservation( LocalDate date, ReservationTime reservationTime, - Theme theme, + ThemeEntity theme, Member member, ReservationStatus status ) { @@ -61,7 +61,7 @@ public class Reservation { Long id, LocalDate date, ReservationTime reservationTime, - Theme theme, + ThemeEntity theme, Member member, ReservationStatus status ) { @@ -74,7 +74,7 @@ public class Reservation { this.reservationStatus = status; } - private void validateIsNull(LocalDate date, ReservationTime reservationTime, Theme theme, Member member, + private void validateIsNull(LocalDate date, ReservationTime reservationTime, ThemeEntity theme, Member member, ReservationStatus reservationStatus) { if (date == null || reservationTime == null || theme == null || member == null || reservationStatus == null) { throw new RoomescapeException(ErrorType.REQUEST_DATA_BLANK, String.format("[values: %s]", this), @@ -98,7 +98,7 @@ public class Reservation { return reservationTime; } - public Theme getTheme() { + public ThemeEntity getTheme() { return theme; } diff --git a/src/main/java/roomescape/reservation/dto/response/ReservationResponse.java b/src/main/java/roomescape/reservation/dto/response/ReservationResponse.java index ba4ecb2b..f1fa1704 100644 --- a/src/main/java/roomescape/reservation/dto/response/ReservationResponse.java +++ b/src/main/java/roomescape/reservation/dto/response/ReservationResponse.java @@ -8,7 +8,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import roomescape.member.web.MemberResponse; import roomescape.reservation.domain.Reservation; import roomescape.reservation.domain.ReservationStatus; -import roomescape.theme.dto.ThemeResponse; +import roomescape.theme.web.ThemeResponse; @Schema(name = "예약 정보", description = "예약 저장 및 조회 응답에 사용됩니다.") public record ReservationResponse( diff --git a/src/main/java/roomescape/reservation/service/ReservationService.java b/src/main/java/roomescape/reservation/service/ReservationService.java index 5b8c4efd..0a3098ac 100644 --- a/src/main/java/roomescape/reservation/service/ReservationService.java +++ b/src/main/java/roomescape/reservation/service/ReservationService.java @@ -24,8 +24,8 @@ import roomescape.reservation.dto.request.WaitingRequest; import roomescape.reservation.dto.response.MyReservationsResponse; import roomescape.reservation.dto.response.ReservationResponse; import roomescape.reservation.dto.response.ReservationsResponse; -import roomescape.theme.domain.Theme; -import roomescape.theme.service.ThemeService; +import roomescape.theme.infrastructure.persistence.ThemeEntity; +import roomescape.theme.business.ThemeService; @Service @Transactional @@ -146,7 +146,7 @@ public class ReservationService { private Reservation getReservationForSave(Long timeId, Long themeId, LocalDate date, Long memberId, ReservationStatus status) { ReservationTime time = reservationTimeService.findTimeById(timeId); - Theme theme = themeService.findThemeById(themeId); + ThemeEntity theme = themeService.findThemeById(themeId); Member member = memberService.findById(memberId); validateDateAndTime(date, time); diff --git a/src/main/java/roomescape/theme/business/ThemeService.kt b/src/main/java/roomescape/theme/business/ThemeService.kt new file mode 100644 index 00000000..1bb1199e --- /dev/null +++ b/src/main/java/roomescape/theme/business/ThemeService.kt @@ -0,0 +1,74 @@ +package roomescape.theme.business + +import org.springframework.data.repository.findByIdOrNull +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import roomescape.common.exception.ErrorType +import roomescape.common.exception.RoomescapeException +import roomescape.theme.infrastructure.persistence.ThemeEntity +import roomescape.theme.infrastructure.persistence.ThemeRepository +import roomescape.theme.web.ThemeRequest +import roomescape.theme.web.ThemeResponse +import roomescape.theme.web.ThemesResponse +import roomescape.theme.web.toResponse +import java.time.LocalDate + +@Service +class ThemeService( + private val themeRepository: ThemeRepository +) { + @Transactional(readOnly = true) + fun findThemeById(id: Long): ThemeEntity = themeRepository.findByIdOrNull(id) + ?: throw RoomescapeException( + ErrorType.THEME_NOT_FOUND, + "[themeId: $id]", + HttpStatus.BAD_REQUEST + ) + + @Transactional(readOnly = true) + fun findAllThemes(): ThemesResponse = themeRepository.findAll() + .toResponse() + + + @Transactional(readOnly = true) + fun getMostReservedThemesByCount(count: Int): ThemesResponse { + val today = LocalDate.now() + val startDate = today.minusDays(7) + val endDate = today.minusDays(1) + + return themeRepository.findTopNThemeBetweenStartDateAndEndDate(startDate, endDate, count) + .toResponse() + } + + @Transactional + fun save(request: ThemeRequest): ThemeResponse { + if (themeRepository.existsByName(request.name)) { + throw RoomescapeException( + ErrorType.THEME_DUPLICATED, + "[name: ${request.name}]", + HttpStatus.CONFLICT + ) + } + + return ThemeEntity( + name = request.name, + description = request.description, + thumbnail = request.thumbnail + ).also { + themeRepository.save(it) + }.toResponse() + } + + @Transactional + fun deleteById(id: Long) { + if (themeRepository.isReservedTheme(id)) { + throw RoomescapeException( + ErrorType.THEME_IS_USED_CONFLICT, + "[themeId: %d]", + HttpStatus.CONFLICT + ) + } + themeRepository.deleteById(id) + } +} diff --git a/src/main/java/roomescape/theme/controller/ThemeController.java b/src/main/java/roomescape/theme/controller/ThemeController.java deleted file mode 100644 index 224190d5..00000000 --- a/src/main/java/roomescape/theme/controller/ThemeController.java +++ /dev/null @@ -1,101 +0,0 @@ -package roomescape.theme.controller; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -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.servlet.http.HttpServletResponse; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; -import roomescape.auth.web.support.Admin; -import roomescape.auth.web.support.LoginRequired; -import roomescape.common.dto.response.RoomescapeApiResponse; -import roomescape.common.dto.response.RoomescapeErrorResponse; -import roomescape.theme.dto.ThemeRequest; -import roomescape.theme.dto.ThemeResponse; -import roomescape.theme.dto.ThemesResponse; -import roomescape.theme.service.ThemeService; - -@RestController -@Tag(name = "5. 테마 API", description = "테마를 조회 / 추가 / 삭제할 때 사용합니다.") -public class ThemeController { - - private final ThemeService themeService; - - public ThemeController(ThemeService themeService) { - this.themeService = themeService; - } - - @LoginRequired - @GetMapping("/themes") - @ResponseStatus(HttpStatus.OK) - @Operation(summary = "모든 테마 조회", description = "모든 테마를 조회합니다.", tags = "로그인이 필요한 API") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true) - }) - public RoomescapeApiResponse getAllThemes() { - return RoomescapeApiResponse.success(themeService.findAllThemes()); - } - - @GetMapping("/themes/most-reserved-last-week") - @ResponseStatus(HttpStatus.OK) - @Operation(summary = "가장 많이 예약된 테마 조회") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true) - }) - public RoomescapeApiResponse getMostReservedThemes( - @RequestParam(defaultValue = "10") @Parameter(description = "최대로 조회할 테마 갯수") int count - ) { - return RoomescapeApiResponse.success(themeService.getMostReservedThemesByCount(count)); - } - - @Admin - @PostMapping("/themes") - @ResponseStatus(HttpStatus.CREATED) - @Operation(summary = "테마 추가", tags = "관리자 로그인이 필요한 API") - @ApiResponses({ - @ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true), - @ApiResponse(responseCode = "409", description = "같은 이름의 테마를 추가할 수 없습니다.", - content = @Content(schema = @Schema(implementation = RoomescapeErrorResponse.class))) - }) - public RoomescapeApiResponse saveTheme( - @Valid @RequestBody ThemeRequest request, - HttpServletResponse response - ) { - ThemeResponse themeResponse = themeService.addTheme(request); - response.setHeader(HttpHeaders.LOCATION, "/themes/" + themeResponse.id()); - - return RoomescapeApiResponse.success(themeResponse); - } - - @Admin - @DeleteMapping("/themes/{id}") - @ResponseStatus(HttpStatus.NO_CONTENT) - @Operation(summary = "테마 삭제", tags = "관리자 로그인이 필요한 API") - @ApiResponses({ - @ApiResponse(responseCode = "204", description = "성공", useReturnTypeSchema = true), - @ApiResponse(responseCode = "409", description = "예약된 테마는 삭제할 수 없습니다.", - content = @Content(schema = @Schema(implementation = RoomescapeErrorResponse.class))) - }) - public RoomescapeApiResponse removeTheme( - @NotNull(message = "themeId는 null일 수 없습니다.") @PathVariable Long id - ) { - themeService.removeThemeById(id); - - return RoomescapeApiResponse.success(); - } -} diff --git a/src/main/java/roomescape/theme/docs/ThemeAPI.kt b/src/main/java/roomescape/theme/docs/ThemeAPI.kt new file mode 100644 index 00000000..624d830a --- /dev/null +++ b/src/main/java/roomescape/theme/docs/ThemeAPI.kt @@ -0,0 +1,51 @@ +package roomescape.theme.docs + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +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.theme.web.ThemeRequest +import roomescape.theme.web.ThemeResponse +import roomescape.theme.web.ThemesResponse + +@Tag(name = "5. 테마 API", description = "테마를 조회 / 추가 / 삭제할 때 사용합니다.") +interface ThemeAPI { + + @LoginRequired + @Operation(summary = "모든 테마 조회", description = "모든 테마를 조회합니다.", tags = ["로그인이 필요한 API"]) + @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) + fun getAllThemes(): ResponseEntity> + + @Operation(summary = "가장 많이 예약된 테마 조회") + @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) + fun getMostReservedThemes( + @RequestParam(defaultValue = "10") @Parameter(description = "최대로 조회할 테마 갯수") count: Int + ): ResponseEntity> + + @Admin + @Operation(summary = "테마 추가", tags = ["관리자 로그인이 필요한 API"]) + @ApiResponses( + ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true), + ) + fun saveTheme( + @Valid @RequestBody request: ThemeRequest, + ): ResponseEntity> + + @Admin + @Operation(summary = "테마 삭제", tags = ["관리자 로그인이 필요한 API"]) + @ApiResponses( + ApiResponse(responseCode = "204", description = "성공", useReturnTypeSchema = true), + ) + fun removeTheme( + @PathVariable id: Long + ): ResponseEntity> +} diff --git a/src/main/java/roomescape/theme/domain/Theme.java b/src/main/java/roomescape/theme/domain/Theme.java deleted file mode 100644 index 19513cd9..00000000 --- a/src/main/java/roomescape/theme/domain/Theme.java +++ /dev/null @@ -1,65 +0,0 @@ -package roomescape.theme.domain; - -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; - -@Entity -public class Theme { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String name; - - private String description; - - private String thumbnail; - - protected Theme() { - } - - public Theme(String name, String description, String thumbnail) { - this(null, name, description, thumbnail); - } - - public Theme( - Long id, - String name, - String description, - String thumbnail - ) { - this.id = id; - this.name = name; - this.description = description; - this.thumbnail = thumbnail; - } - - public Long getId() { - return id; - } - - public String getName() { - return name; - } - - public String getDescription() { - return description; - } - - public String getThumbnail() { - return thumbnail; - } - - @Override - public String toString() { - return "Theme{" + - "id=" + id + - ", name=" + name + - ", description=" + description + - ", thumbnail=" + thumbnail + - '}'; - } -} diff --git a/src/main/java/roomescape/theme/domain/repository/ThemeRepository.java b/src/main/java/roomescape/theme/domain/repository/ThemeRepository.java deleted file mode 100644 index 17e5f491..00000000 --- a/src/main/java/roomescape/theme/domain/repository/ThemeRepository.java +++ /dev/null @@ -1,34 +0,0 @@ -package roomescape.theme.domain.repository; - -import java.time.LocalDate; -import java.util.List; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; - -import roomescape.theme.domain.Theme; - -public interface ThemeRepository extends JpaRepository { - - @Query(value = """ - SELECT t - FROM Theme t - RIGHT JOIN Reservation r ON t.id = r.theme.id - WHERE r.date BETWEEN :startDate AND :endDate - GROUP BY r.theme.id - ORDER BY COUNT(r.theme.id) DESC, t.id ASC - LIMIT :limit - """) - List findTopNThemeBetweenStartDateAndEndDate(LocalDate startDate, LocalDate endDate, int limit); - - boolean existsByName(String name); - - @Query(value = """ - SELECT EXISTS( - SELECT 1 - FROM Reservation r - WHERE r.theme.id = :id - ) - """) - boolean isReservedTheme(Long id); -} diff --git a/src/main/java/roomescape/theme/dto/ThemeRequest.java b/src/main/java/roomescape/theme/dto/ThemeRequest.java deleted file mode 100644 index 438c9645..00000000 --- a/src/main/java/roomescape/theme/dto/ThemeRequest.java +++ /dev/null @@ -1,21 +0,0 @@ -package roomescape.theme.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Size; - -@Schema(name = "테마 저장 요청", description = "테마 정보를 저장할 때 사용합니다.") -public record ThemeRequest( - @NotBlank(message = "테마의 이름은 null 또는 공백일 수 없습니다.") - @Size(min = 1, max = 20, message = "테마의 이름은 1~20글자 사이여야 합니다.") - @Schema(description = "필수 값이며, 최대 20글자까지 입력 가능합니다.") - String name, - @NotBlank(message = "테마의 설명은 null 또는 공백일 수 없습니다.") - @Size(min = 1, max = 100, message = "테마의 설명은 1~100글자 사이여야 합니다.") - @Schema(description = "필수 값이며, 최대 100글자까지 입력 가능합니다.") - String description, - @NotBlank(message = "테마의 쌈네일은 null 또는 공백일 수 없습니다.") - @Schema(description = "필수 값이며, 썸네일 이미지 URL 을 입력해주세요.") - String thumbnail -) { -} diff --git a/src/main/java/roomescape/theme/dto/ThemeResponse.java b/src/main/java/roomescape/theme/dto/ThemeResponse.java deleted file mode 100644 index f95ab31b..00000000 --- a/src/main/java/roomescape/theme/dto/ThemeResponse.java +++ /dev/null @@ -1,21 +0,0 @@ -package roomescape.theme.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import roomescape.theme.domain.Theme; - -@Schema(name = "테마 정보", description = "테마 추가 및 조회 응답에 사용됩니다.") -public record ThemeResponse( - @Schema(description = "테마 번호. 테마를 식별할 때 사용합니다.") - Long id, - @Schema(description = "테마 이름. 중복을 허용하지 않습니다.") - String name, - @Schema(description = "테마 설명") - String description, - @Schema(description = "테마 썸네일 이미지 URL") - String thumbnail -) { - - public static ThemeResponse from(Theme theme) { - return new ThemeResponse(theme.getId(), theme.getName(), theme.getDescription(), theme.getThumbnail()); - } -} diff --git a/src/main/java/roomescape/theme/dto/ThemesResponse.java b/src/main/java/roomescape/theme/dto/ThemesResponse.java deleted file mode 100644 index 2ea88862..00000000 --- a/src/main/java/roomescape/theme/dto/ThemesResponse.java +++ /dev/null @@ -1,11 +0,0 @@ -package roomescape.theme.dto; - -import java.util.List; - -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(name = "테마 목록 조회 응답", description = "모든 테마 목록 조회 응답시 사용됩니다.") -public record ThemesResponse( - @Schema(description = "모든 테마 목록") List themes -) { -} diff --git a/src/main/java/roomescape/theme/infrastructure/persistence/ThemeEntity.kt b/src/main/java/roomescape/theme/infrastructure/persistence/ThemeEntity.kt new file mode 100644 index 00000000..6d8e3d79 --- /dev/null +++ b/src/main/java/roomescape/theme/infrastructure/persistence/ThemeEntity.kt @@ -0,0 +1,15 @@ +package roomescape.theme.infrastructure.persistence + +import jakarta.persistence.* + +@Entity +@Table(name = "theme") +class ThemeEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + + var name: String, + var description: String, + var thumbnail: String +) \ No newline at end of file diff --git a/src/main/java/roomescape/theme/infrastructure/persistence/ThemeRepository.kt b/src/main/java/roomescape/theme/infrastructure/persistence/ThemeRepository.kt new file mode 100644 index 00000000..f0ea0b5a --- /dev/null +++ b/src/main/java/roomescape/theme/infrastructure/persistence/ThemeRepository.kt @@ -0,0 +1,32 @@ +package roomescape.theme.infrastructure.persistence + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import java.time.LocalDate + +interface ThemeRepository : JpaRepository { + + @Query(value = """ + SELECT t + FROM ThemeEntity t + RIGHT JOIN Reservation r ON t.id = r.theme.id + WHERE r.date BETWEEN :startDate AND :endDate + GROUP BY r.theme.id + ORDER BY COUNT(r.theme.id) DESC, t.id ASC + LIMIT :limit + + """ + ) + fun findTopNThemeBetweenStartDateAndEndDate(startDate: LocalDate, endDate: LocalDate, limit: Int): List + + fun existsByName(name: String): Boolean + + @Query(value = """ + SELECT EXISTS( + SELECT 1 + FROM Reservation r + WHERE r.theme.id = :id + ) + """) + fun isReservedTheme(id: Long): Boolean +} diff --git a/src/main/java/roomescape/theme/service/ThemeService.java b/src/main/java/roomescape/theme/service/ThemeService.java deleted file mode 100644 index aabc2790..00000000 --- a/src/main/java/roomescape/theme/service/ThemeService.java +++ /dev/null @@ -1,82 +0,0 @@ -package roomescape.theme.service; - -import java.time.LocalDate; -import java.util.List; - -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import roomescape.common.exception.ErrorType; -import roomescape.common.exception.RoomescapeException; -import roomescape.reservation.domain.repository.ReservationRepository; -import roomescape.theme.domain.Theme; -import roomescape.theme.domain.repository.ThemeRepository; -import roomescape.theme.dto.ThemeRequest; -import roomescape.theme.dto.ThemeResponse; -import roomescape.theme.dto.ThemesResponse; - -@Service -@Transactional -public class ThemeService { - - private final ThemeRepository themeRepository; - - public ThemeService(ThemeRepository themeRepository) { - this.themeRepository = themeRepository; - } - - @Transactional(readOnly = true) - public Theme findThemeById(Long id) { - return themeRepository.findById(id) - .orElseThrow(() -> new RoomescapeException(ErrorType.THEME_NOT_FOUND, - String.format("[themeId: %d]", id), HttpStatus.BAD_REQUEST)); - } - - @Transactional(readOnly = true) - public ThemesResponse findAllThemes() { - List response = themeRepository.findAll() - .stream() - .map(ThemeResponse::from) - .toList(); - - return new ThemesResponse(response); - } - - @Transactional(readOnly = true) - public ThemesResponse getMostReservedThemesByCount(int count) { - LocalDate today = LocalDate.now(); - LocalDate startDate = today.minusDays(7); - LocalDate endDate = today.minusDays(1); - - List response = themeRepository.findTopNThemeBetweenStartDateAndEndDate(startDate, endDate, - count) - .stream() - .map(ThemeResponse::from) - .toList(); - - return new ThemesResponse(response); - } - - public ThemeResponse addTheme(ThemeRequest request) { - validateIsSameThemeNameExist(request.name()); - Theme theme = themeRepository.save(new Theme(request.name(), request.description(), request.thumbnail())); - - return ThemeResponse.from(theme); - } - - private void validateIsSameThemeNameExist(String name) { - if (themeRepository.existsByName(name)) { - throw new RoomescapeException(ErrorType.THEME_DUPLICATED, - String.format("[name: %s]", name), HttpStatus.CONFLICT); - } - } - - public void removeThemeById(Long id) { - if (themeRepository.isReservedTheme(id)) { - throw new RoomescapeException(ErrorType.THEME_IS_USED_CONFLICT, - String.format("[themeId: %d]", id), HttpStatus.CONFLICT); - } - themeRepository.deleteById(id); - } -} diff --git a/src/main/java/roomescape/theme/web/ThemeController.kt b/src/main/java/roomescape/theme/web/ThemeController.kt new file mode 100644 index 00000000..1a7a7ec1 --- /dev/null +++ b/src/main/java/roomescape/theme/web/ThemeController.kt @@ -0,0 +1,51 @@ +package roomescape.theme.web + +import io.swagger.v3.oas.annotations.Parameter +import jakarta.validation.Valid +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* +import roomescape.common.dto.response.CommonApiResponse +import roomescape.theme.business.ThemeService +import roomescape.theme.docs.ThemeAPI +import java.net.URI + +@RestController +class ThemeController( + private val themeService: ThemeService +) : ThemeAPI { + + @GetMapping("/themes") + override fun getAllThemes(): ResponseEntity> { + val response: ThemesResponse = themeService.findAllThemes() + + return ResponseEntity.ok(CommonApiResponse(response)) + } + + @GetMapping("/themes/most-reserved-last-week") + override fun getMostReservedThemes( + @RequestParam(defaultValue = "10") @Parameter(description = "최대로 조회할 테마 갯수") count: Int + ): ResponseEntity> { + val response: ThemesResponse = themeService.getMostReservedThemesByCount(count) + + return ResponseEntity.ok(CommonApiResponse(response)) + } + + @PostMapping("/themes") + override fun saveTheme( + @RequestBody @Valid request: ThemeRequest + ): ResponseEntity> { + val themeResponse: ThemeResponse = themeService.save(request) + + return ResponseEntity.created(URI.create("/themes/${themeResponse.id}")) + .body(CommonApiResponse(themeResponse)) + } + + @DeleteMapping("/themes/{id}") + override fun removeTheme( + @PathVariable id: Long + ): ResponseEntity> { + themeService.deleteById(id) + + return ResponseEntity.noContent().build() + } +} diff --git a/src/main/java/roomescape/theme/web/ThemeDTO.kt b/src/main/java/roomescape/theme/web/ThemeDTO.kt new file mode 100644 index 00000000..27c4ea61 --- /dev/null +++ b/src/main/java/roomescape/theme/web/ThemeDTO.kt @@ -0,0 +1,68 @@ +package roomescape.theme.web + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.Size +import org.hibernate.validator.constraints.URL +import roomescape.theme.infrastructure.persistence.ThemeEntity + +@Schema(name = "테마 저장 요청", description = "테마 정보를 저장할 때 사용합니다.") +@JvmRecord +data class ThemeRequest( + @field:Schema(description = "필수 값이며, 최대 20글자까지 입력 가능합니다.") + @NotBlank + @Size(max = 20, message = "테마의 이름은 1~20글자 사이여야 합니다.") + val name: String, + + @field:Schema(description = "필수 값이며, 최대 100글자까지 입력 가능합니다.") + @NotBlank + @Size(max = 100, message = "테마의 설명은 1~100글자 사이여야 합니다.") + val description: String, + + @field:Schema(description = "필수 값이며, 썸네일 이미지 URL 을 입력해주세요.") + @NotBlank + @URL + val thumbnail: String +) + +@Schema(name = "테마 정보", description = "테마 추가 및 조회 응답에 사용됩니다.") +@JvmRecord +data class ThemeResponse( + @field:Schema(description = "테마 번호. 테마를 식별할 때 사용합니다.") + val id: Long, + + @field:Schema(description = "테마 이름. 중복을 허용하지 않습니다.") + val name: String, + + @field:Schema(description = "테마 설명") + val description: String, + + @field:Schema(description = "테마 썸네일 이미지 URL") + val thumbnail: String +) { + companion object { + @JvmStatic + fun from(themeEntity: ThemeEntity): ThemeResponse { + return ThemeResponse(themeEntity.id!!, themeEntity.name, themeEntity.description, themeEntity.thumbnail) + } + } +} + +fun ThemeEntity.toResponse(): ThemeResponse = ThemeResponse( + id = this.id!!, + name = this.name, + description = this.description, + thumbnail = this.thumbnail +) + + +@Schema(name = "테마 목록 조회 응답", description = "모든 테마 목록 조회 응답시 사용됩니다.") +@JvmRecord +data class ThemesResponse( + @field:Schema(description = "모든 테마 목록") + val themes: List +) + +fun List.toResponse(): ThemesResponse = ThemesResponse( + themes = this.map { it.toResponse()} +) diff --git a/src/test/java/roomescape/payment/business/PaymentServiceTest.java b/src/test/java/roomescape/payment/business/PaymentServiceTest.java index 10a5dbb8..57f057a8 100644 --- a/src/test/java/roomescape/payment/business/PaymentServiceTest.java +++ b/src/test/java/roomescape/payment/business/PaymentServiceTest.java @@ -27,8 +27,8 @@ import roomescape.reservation.domain.ReservationStatus; import roomescape.reservation.domain.ReservationTime; import roomescape.reservation.domain.repository.ReservationRepository; import roomescape.reservation.domain.repository.ReservationTimeRepository; -import roomescape.theme.domain.Theme; -import roomescape.theme.domain.repository.ThemeRepository; +import roomescape.theme.infrastructure.persistence.ThemeEntity; +import roomescape.theme.infrastructure.persistence.ThemeRepository; @SpringBootTest @Sql(scripts = "/truncate.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @@ -57,7 +57,7 @@ class PaymentServiceTest { LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); Member member = memberRepository.save(new Member(null, "member", "email@email.com", "password", Role.MEMBER)); - Theme theme = themeRepository.save(new Theme("name", "desc", "thumbnail")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "desc", "thumbnail")); Reservation reservation = reservationRepository.save(new Reservation(date, time, theme, member, ReservationStatus.CONFIRMED)); @@ -79,7 +79,7 @@ class PaymentServiceTest { LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); Member member = memberRepository.save(new Member(null, "member", "email@email.com", "password", Role.MEMBER)); - Theme theme = themeRepository.save(new Theme("name", "desc", "thumbnail")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "desc", "thumbnail")); Reservation reservation = reservationRepository.save(new Reservation(date, time, theme, member, ReservationStatus.CONFIRMED)); @@ -116,7 +116,7 @@ class PaymentServiceTest { LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); Member member = memberRepository.save(new Member(null, "member", "email@email.com", "password", Role.MEMBER)); - Theme theme = themeRepository.save(new Theme("name", "desc", "thumbnail")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "desc", "thumbnail")); Reservation reservation = reservationRepository.save(new Reservation(date, time, theme, member, ReservationStatus.CONFIRMED)); diff --git a/src/test/java/roomescape/reservation/controller/ReservationControllerTest.java b/src/test/java/roomescape/reservation/controller/ReservationControllerTest.java index a026ee41..5a888a55 100644 --- a/src/test/java/roomescape/reservation/controller/ReservationControllerTest.java +++ b/src/test/java/roomescape/reservation/controller/ReservationControllerTest.java @@ -2,7 +2,6 @@ package roomescape.reservation.controller; import static org.assertj.core.api.Assertions.*; import static org.hamcrest.Matchers.*; -import static org.mockito.ArgumentMatchers.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @@ -12,7 +11,6 @@ import java.time.LocalTime; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.Map; -import java.util.Optional; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; @@ -51,8 +49,8 @@ import roomescape.reservation.domain.repository.ReservationTimeRepository; import roomescape.reservation.dto.request.AdminReservationRequest; import roomescape.reservation.dto.request.ReservationRequest; import roomescape.reservation.dto.request.WaitingRequest; -import roomescape.theme.domain.Theme; -import roomescape.theme.domain.repository.ThemeRepository; +import roomescape.theme.infrastructure.persistence.ThemeEntity; +import roomescape.theme.infrastructure.persistence.ThemeRepository; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Sql(scripts = "/truncate.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @@ -91,7 +89,7 @@ public class ReservationControllerTest { LocalDate date = LocalDate.now().plusDays(1L); reservationTimeRepository.save(new ReservationTime(time)); - themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Map reservationParams = Map.of( "date", date.toString(), @@ -126,7 +124,7 @@ public class ReservationControllerTest { String accessTokenCookie = getAccessTokenCookieByLogin("email@email.com", "password"); ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30))); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member member1 = memberRepository.save(new Member(null, "name1", "email1r@email.com", "password", Role.MEMBER)); // when @@ -154,7 +152,7 @@ public class ReservationControllerTest { String accessTokenCookie = getAccessTokenCookieByLogin("email@email.com", "password"); ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30))); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member waitingMember = memberRepository.save( new Member(null, "name1", "email1r@email.com", "password", Role.MEMBER)); @@ -181,7 +179,7 @@ public class ReservationControllerTest { String accessTokenCookie = getAdminAccessTokenCookieByLogin("admin@admin.com", "12341234"); ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30))); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); // when @@ -210,7 +208,7 @@ public class ReservationControllerTest { String accessTokenCookie = getAccessTokenCookieByLogin(member.getEmail(), member.getPassword()); ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30))); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Reservation reservation = reservationRepository.save( new Reservation(LocalDate.now(), reservationTime, theme, member, ReservationStatus.CONFIRMED)); @@ -230,7 +228,7 @@ public class ReservationControllerTest { String adminTokenCookie = getAdminAccessTokenCookieByLogin("admin@email.com", "password"); ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30))); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member confirmedMember = memberRepository.save( new Member(null, "name1", "email@email.com", "password", Role.MEMBER)); Member waitingMember = memberRepository.save( @@ -258,7 +256,7 @@ public class ReservationControllerTest { String accessTokenCookie = getAccessTokenCookieByLogin(member.getEmail(), member.getPassword()); ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30))); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member anotherMember = memberRepository.save( new Member(null, "name", "email@email.com", "password", Role.MEMBER)); @@ -339,7 +337,7 @@ public class ReservationControllerTest { void getAllReservations(String requestURI, String responseFieldName, int expectedSize) { // given LocalDate date = LocalDate.now().plusDays(1); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); ReservationTime time = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30))); ReservationTime time1 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(18, 30))); ReservationTime time2 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(19, 30))); @@ -368,7 +366,7 @@ public class ReservationControllerTest { void removeNotPaidReservation() { // given LocalDate date = LocalDate.now().plusDays(1); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); ReservationTime time = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30))); String accessToken = getAdminAccessTokenCookieByLogin("admin@email.com", "password"); @@ -392,7 +390,7 @@ public class ReservationControllerTest { // given String accessToken = getAdminAccessTokenCookieByLogin("admin@email.com", "password"); LocalDate date = LocalDate.now().plusDays(1); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); ReservationTime time = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30))); Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); @@ -423,7 +421,7 @@ public class ReservationControllerTest { LocalDateTime localDateTime = LocalDateTime.now().minusHours(1L).withNano(0); LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); String accessToken = getAccessTokenCookieByLogin(member.getEmail(), member.getPassword()); @@ -516,7 +514,7 @@ public class ReservationControllerTest { LocalDateTime localDateTime = LocalDateTime.now().plusDays(1L).withNano(0); LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); Member member1 = memberRepository.save(new Member(null, "name1", "email1@email.com", "password", Role.MEMBER)); @@ -542,7 +540,7 @@ public class ReservationControllerTest { LocalDateTime localDateTime = LocalDateTime.now().plusHours(1L); LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); String accessToken = getAdminAccessTokenCookieByLogin("admin@email.com", "password"); @@ -587,7 +585,7 @@ public class ReservationControllerTest { LocalDateTime localDateTime = LocalDateTime.now().plusDays(1L).withNano(0); LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); String adminAccessToken = getAdminAccessTokenCookieByLogin("admin@email.com", "password"); diff --git a/src/test/java/roomescape/reservation/controller/ReservationTimeControllerTest.java b/src/test/java/roomescape/reservation/controller/ReservationTimeControllerTest.java index 9f809fe9..baaa53a3 100644 --- a/src/test/java/roomescape/reservation/controller/ReservationTimeControllerTest.java +++ b/src/test/java/roomescape/reservation/controller/ReservationTimeControllerTest.java @@ -29,8 +29,8 @@ import roomescape.reservation.domain.ReservationStatus; import roomescape.reservation.domain.ReservationTime; import roomescape.reservation.domain.repository.ReservationRepository; import roomescape.reservation.domain.repository.ReservationTimeRepository; -import roomescape.theme.domain.Theme; -import roomescape.theme.domain.repository.ThemeRepository; +import roomescape.theme.infrastructure.persistence.ThemeEntity; +import roomescape.theme.infrastructure.persistence.ThemeRepository; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @Sql(scripts = "/truncate.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @@ -224,7 +224,7 @@ public class ReservationTimeControllerTest { ReservationTime reservationTime1 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 0))); ReservationTime reservationTime2 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30))); ReservationTime reservationTime3 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(18, 30))); - Theme theme = themeRepository.save(new Theme("테마명1", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명1", "설명", "썸네일URL")); Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); reservationRepository.save( diff --git a/src/test/java/roomescape/reservation/domain/ReservationTest.java b/src/test/java/roomescape/reservation/domain/ReservationTest.java index ba1bc491..6f19375d 100644 --- a/src/test/java/roomescape/reservation/domain/ReservationTest.java +++ b/src/test/java/roomescape/reservation/domain/ReservationTest.java @@ -13,14 +13,14 @@ import org.junit.jupiter.params.provider.MethodSource; import roomescape.member.infrastructure.persistence.Member; import roomescape.member.infrastructure.persistence.Role; import roomescape.common.exception.RoomescapeException; -import roomescape.theme.domain.Theme; +import roomescape.theme.infrastructure.persistence.ThemeEntity; public class ReservationTest { @ParameterizedTest @MethodSource("validateConstructorParameterBlankSource") @DisplayName("객체 생성 시, null 또는 공백이 존재하면 예외를 발생한다.") - void validateConstructorParameterBlank(LocalDate date, ReservationTime reservationTime, Theme theme, + void validateConstructorParameterBlank(LocalDate date, ReservationTime reservationTime, ThemeEntity theme, Member member) { // when & then @@ -33,12 +33,12 @@ public class ReservationTest { return Stream.of( Arguments.of(null, new ReservationTime(LocalTime.now().plusHours(1)), - new Theme("테마명", "설명", "썸네일URI"), + new ThemeEntity(null, "테마명", "설명", "썸네일URI"), new Member(null, "name", "email@email.com", "password", Role.MEMBER)), Arguments.of( LocalDate.now(), null, - new Theme("테마명", "설명", "썸네일URI"), + new ThemeEntity(null, "테마명", "설명", "썸네일URI"), new Member(null, "name", "email@email.com", "password", Role.MEMBER)), Arguments.of( LocalDate.now(), @@ -48,7 +48,7 @@ public class ReservationTest { Arguments.of( LocalDate.now(), new ReservationTime(LocalTime.now().plusHours(1)), - new Theme("테마명", "설명", "썸네일URI"), + new ThemeEntity(null, "테마명", "설명", "썸네일URI"), null) ); } diff --git a/src/test/java/roomescape/reservation/domain/repository/ReservationSearchSpecificationTest.java b/src/test/java/roomescape/reservation/domain/repository/ReservationSearchSpecificationTest.java index 89dda95c..56f33293 100644 --- a/src/test/java/roomescape/reservation/domain/repository/ReservationSearchSpecificationTest.java +++ b/src/test/java/roomescape/reservation/domain/repository/ReservationSearchSpecificationTest.java @@ -19,8 +19,8 @@ import roomescape.member.infrastructure.persistence.Role; import roomescape.reservation.domain.Reservation; import roomescape.reservation.domain.ReservationStatus; import roomescape.reservation.domain.ReservationTime; -import roomescape.theme.domain.Theme; -import roomescape.theme.domain.repository.ThemeRepository; +import roomescape.theme.infrastructure.persistence.ThemeEntity; +import roomescape.theme.infrastructure.persistence.ThemeRepository; @DataJpaTest class ReservationSearchSpecificationTest { @@ -52,7 +52,7 @@ class ReservationSearchSpecificationTest { LocalDateTime dateTime = LocalDateTime.now(); Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); ReservationTime time = timeRepository.save(new ReservationTime(dateTime.toLocalTime())); - Theme theme = themeRepository.save(new Theme("name", "description", "thumbnail")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "description", "thumbnail")); reservation1 = reservationRepository.save( new Reservation(dateTime.toLocalDate(), time, theme, member, ReservationStatus.CONFIRMED)); diff --git a/src/test/java/roomescape/reservation/service/ReservationServiceTest.java b/src/test/java/roomescape/reservation/service/ReservationServiceTest.java index cd3cb65b..b85aa741 100644 --- a/src/test/java/roomescape/reservation/service/ReservationServiceTest.java +++ b/src/test/java/roomescape/reservation/service/ReservationServiceTest.java @@ -27,9 +27,9 @@ import roomescape.reservation.dto.request.ReservationRequest; import roomescape.reservation.dto.request.WaitingRequest; import roomescape.reservation.dto.response.ReservationResponse; import roomescape.common.exception.RoomescapeException; -import roomescape.theme.domain.Theme; -import roomescape.theme.domain.repository.ThemeRepository; -import roomescape.theme.service.ThemeService; +import roomescape.theme.infrastructure.persistence.ThemeEntity; +import roomescape.theme.infrastructure.persistence.ThemeRepository; +import roomescape.theme.business.ThemeService; @SpringBootTest @Sql(scripts = "/truncate.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @@ -52,7 +52,7 @@ class ReservationServiceTest { void reservationAlreadyExistFail() { // given ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30))); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member member1 = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); Member member2 = memberRepository.save(new Member(null, "name2", "email2@email.com", "password", Role.MEMBER)); LocalDate date = LocalDate.now().plusDays(1L); @@ -74,7 +74,7 @@ class ReservationServiceTest { void requestWaitWhenAlreadyReserveFail() { // given ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30))); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); LocalDate date = LocalDate.now().plusDays(1L); @@ -94,7 +94,7 @@ class ReservationServiceTest { void requestWaitTwiceFail() { // given ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30))); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); Member member1 = memberRepository.save(new Member(null, "name1", "email1@email.com", "password", Role.MEMBER)); LocalDate date = LocalDate.now().plusDays(1L); @@ -118,7 +118,7 @@ class ReservationServiceTest { void beforeDateReservationFail() { // given ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30))); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); LocalDate beforeDate = LocalDate.now().minusDays(1L); @@ -135,7 +135,7 @@ class ReservationServiceTest { // given LocalDateTime beforeTime = LocalDateTime.now().minusHours(1L).withNano(0); ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(beforeTime.toLocalTime())); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); // when & then @@ -151,7 +151,7 @@ class ReservationServiceTest { // given LocalDateTime beforeTime = LocalDateTime.now().minusDays(1L).withNano(0); ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(beforeTime.toLocalTime())); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Long NotExistMemberId = 1L; // when & then @@ -179,7 +179,7 @@ class ReservationServiceTest { void confirmWaitingWhenReservationExist() { // given ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30))); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member admin = memberRepository.save(new Member(null, "admin", "admin@email.com", "password", Role.ADMIN)); Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); Member member1 = memberRepository.save(new Member(null, "name1", "email1@email.com", "password", Role.MEMBER)); @@ -202,7 +202,7 @@ class ReservationServiceTest { void approveWaiting() { // given ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30))); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member admin = memberRepository.save(new Member(null, "admin", "admin@email.com", "password", Role.ADMIN)); Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); diff --git a/src/test/java/roomescape/reservation/service/ReservationTimeServiceTest.java b/src/test/java/roomescape/reservation/service/ReservationTimeServiceTest.java index 76d4e97a..473f5698 100644 --- a/src/test/java/roomescape/reservation/service/ReservationTimeServiceTest.java +++ b/src/test/java/roomescape/reservation/service/ReservationTimeServiceTest.java @@ -23,8 +23,8 @@ import roomescape.reservation.domain.repository.ReservationRepository; import roomescape.reservation.domain.repository.ReservationTimeRepository; import roomescape.reservation.dto.request.ReservationTimeRequest; import roomescape.common.exception.RoomescapeException; -import roomescape.theme.domain.Theme; -import roomescape.theme.domain.repository.ThemeRepository; +import roomescape.theme.infrastructure.persistence.ThemeEntity; +import roomescape.theme.infrastructure.persistence.ThemeRepository; @SpringBootTest @Import(ReservationTimeService.class) @@ -74,7 +74,7 @@ class ReservationTimeServiceTest { LocalDateTime localDateTime = LocalDateTime.now().plusDays(1L).withNano(0); ReservationTime reservationTime = reservationTimeRepository.save( new ReservationTime(localDateTime.toLocalTime())); - Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); // when diff --git a/src/test/java/roomescape/reservation/service/ReservationWithPaymentServiceTest.java b/src/test/java/roomescape/reservation/service/ReservationWithPaymentServiceTest.java index b0f589c3..4a7e5cdc 100644 --- a/src/test/java/roomescape/reservation/service/ReservationWithPaymentServiceTest.java +++ b/src/test/java/roomescape/reservation/service/ReservationWithPaymentServiceTest.java @@ -28,8 +28,8 @@ import roomescape.reservation.domain.repository.ReservationRepository; import roomescape.reservation.domain.repository.ReservationTimeRepository; import roomescape.reservation.dto.request.ReservationRequest; import roomescape.reservation.dto.response.ReservationResponse; -import roomescape.theme.domain.Theme; -import roomescape.theme.domain.repository.ThemeRepository; +import roomescape.theme.infrastructure.persistence.ThemeEntity; +import roomescape.theme.infrastructure.persistence.ThemeRepository; @SpringBootTest @Sql(scripts = "/truncate.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @@ -60,7 +60,7 @@ class ReservationWithPaymentServiceTest { LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); Member member = memberRepository.save(new Member(null, "member", "email@email.com", "password", Role.MEMBER)); - Theme theme = themeRepository.save(new Theme("name", "desc", "thumbnail")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "desc", "thumbnail")); ReservationRequest reservationRequest = new ReservationRequest(date, time.getId(), theme.getId(), "payment-key", "order-id", 10000L, "NORMAL"); @@ -96,7 +96,7 @@ class ReservationWithPaymentServiceTest { LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); Member member = memberRepository.save(new Member(null, "member", "admin@email.com", "password", Role.ADMIN)); - Theme theme = themeRepository.save(new Theme("name", "desc", "thumbnail")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "desc", "thumbnail")); ReservationRequest reservationRequest = new ReservationRequest(date, time.getId(), theme.getId(), "payment-key", "order-id", 10000L, "NORMAL"); @@ -124,7 +124,7 @@ class ReservationWithPaymentServiceTest { LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); Member member = memberRepository.save(new Member(null, "member", "admin@email.com", "password", Role.ADMIN)); - Theme theme = themeRepository.save(new Theme("name", "desc", "thumbnail")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "desc", "thumbnail")); Reservation saved = reservationRepository.save( new Reservation(date, time, theme, member, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED)); @@ -146,7 +146,7 @@ class ReservationWithPaymentServiceTest { LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); Member member = memberRepository.save(new Member(null, "member", "admin@email.com", "password", Role.ADMIN)); - Theme theme = themeRepository.save(new Theme("name", "desc", "thumbnail")); + ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "desc", "thumbnail")); ReservationRequest reservationRequest = new ReservationRequest(date, time.getId(), theme.getId(), "payment-key", "order-id", 10000L, "NORMAL"); diff --git a/src/test/java/roomescape/theme/business/ThemeServiceTest.kt b/src/test/java/roomescape/theme/business/ThemeServiceTest.kt new file mode 100644 index 00000000..a244273c --- /dev/null +++ b/src/test/java/roomescape/theme/business/ThemeServiceTest.kt @@ -0,0 +1,103 @@ +package roomescape.theme.business + +import io.kotest.assertions.assertSoftly +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 org.springframework.data.repository.findByIdOrNull +import org.springframework.http.HttpStatus +import roomescape.common.exception.ErrorType +import roomescape.common.exception.RoomescapeException +import roomescape.theme.infrastructure.persistence.ThemeEntity +import roomescape.theme.infrastructure.persistence.ThemeRepository +import roomescape.theme.web.ThemeRequest +import roomescape.util.ThemeFixture + +class ThemeServiceTest : FunSpec({ + + val themeRepository: ThemeRepository = mockk() + val themeService = ThemeService(themeRepository) + + context("findThemeById") { + val themeId = 1L + test("조회 성공") { + val theme: ThemeEntity = ThemeFixture.create(id = themeId) + every { + themeRepository.findByIdOrNull(themeId) + } returns theme + + theme.id shouldBe themeId + } + + test("ID로 테마를 찾을 수 없으면 400 예외를 던진다.") { + every { + themeRepository.findByIdOrNull(themeId) + } returns null + + val exception = shouldThrow { + themeService.findThemeById(themeId) + } + + exception.errorType shouldBe ErrorType.THEME_NOT_FOUND + } + } + + context("findAllThemes") { + test("모든 테마를 조회한다.") { + val themes = listOf(ThemeFixture.create(id = 1, name = "t1"), ThemeFixture.create(id = 2, name = "t2")) + every { + themeRepository.findAll() + } returns themes + + assertSoftly(themeService.findAllThemes()) { + this.themes.size shouldBe themes.size + this.themes[0].name shouldBe "t1" + this.themes[1].name shouldBe "t2" + } + } + } + + context("save") { + test("테마 이름이 중복되면 409 예외를 던진다.") { + val name = "Duplicate Theme" + + every { + themeRepository.existsByName(name) + } returns true + + val exception = shouldThrow { + themeService.save(ThemeRequest( + name = name, + description = "Description", + thumbnail = "http://example.com/thumbnail.jpg" + )) + } + + assertSoftly(exception) { + this.errorType shouldBe ErrorType.THEME_DUPLICATED + this.httpStatus shouldBe HttpStatus.CONFLICT + } + } + } + + context("deleteById") { + test("이미 예약 중인 테마라면 409 예외를 던진다.") { + val themeId = 1L + + every { + themeRepository.isReservedTheme(themeId) + } returns true + + val exception = shouldThrow { + themeService.deleteById(themeId) + } + + assertSoftly(exception) { + this.errorType shouldBe ErrorType.THEME_IS_USED_CONFLICT + this.httpStatus shouldBe HttpStatus.CONFLICT + } + } + } +}) diff --git a/src/test/java/roomescape/theme/controller/ThemeControllerTest.java b/src/test/java/roomescape/theme/controller/ThemeControllerTest.java deleted file mode 100644 index 391ed044..00000000 --- a/src/test/java/roomescape/theme/controller/ThemeControllerTest.java +++ /dev/null @@ -1,184 +0,0 @@ -package roomescape.theme.controller; - -import static org.hamcrest.Matchers.*; - -import java.util.Map; -import java.util.stream.Stream; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.test.context.jdbc.Sql; -import org.springframework.test.context.jdbc.Sql.ExecutionPhase; - -import io.restassured.RestAssured; -import io.restassured.http.ContentType; -import io.restassured.http.Header; -import roomescape.member.infrastructure.persistence.Member; -import roomescape.member.infrastructure.persistence.MemberRepository; -import roomescape.member.infrastructure.persistence.Role; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@Sql(scripts = "/truncate.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) -class ThemeControllerTest { - - @LocalServerPort - private int port; - - @Autowired - private MemberRepository memberRepository; - - @Test - @DisplayName("모든 테마 정보를 조회한다.") - void readThemes() { - String email = "admin@test.com"; - String password = "12341234"; - String adminAccessTokenCookie = getAdminAccessTokenCookieByLogin(email, password); - - RestAssured.given().log().all() - .contentType(ContentType.JSON) - .header(new Header("Cookie", adminAccessTokenCookie)) - .port(port) - .when().get("/themes") - .then().log().all() - .statusCode(200) - .body("data.themes.size()", is(0)); - } - - @Test - @DisplayName("테마를 추가한다.") - void createThemes() { - String email = "admin@test.com"; - String password = "12341234"; - String adminAccessTokenCookie = getAdminAccessTokenCookieByLogin(email, password); - - Map params = Map.of( - "name", "테마명", - "description", "설명", - "thumbnail", "http://testsfasdgasd.com" - ); - - RestAssured.given().log().all() - .contentType(ContentType.JSON) - .header(new Header("Cookie", adminAccessTokenCookie)) - .port(port) - .body(params) - .when().post("/themes") - .then().log().all() - .statusCode(201) - .body("data.id", is(1)) - .header("Location", "/themes/1"); - } - - @Test - @DisplayName("테마를 삭제한다.") - void deleteThemes() { - String email = "admin@test.com"; - String password = "12341234"; - String adminAccessTokenCookie = getAdminAccessTokenCookieByLogin(email, password); - - Map params = Map.of( - "name", "테마명", - "description", "설명", - "thumbnail", "http://testsfasdgasd.com" - ); - - RestAssured.given().log().all() - .contentType(ContentType.JSON) - .header(new Header("Cookie", adminAccessTokenCookie)) - .port(port) - .body(params) - .when().post("/themes") - .then().log().all() - .statusCode(201) - .body("data.id", is(1)) - .header("Location", "/themes/1"); - - RestAssured.given().log().all() - .contentType(ContentType.JSON) - .header(new Header("Cookie", adminAccessTokenCookie)) - .port(port) - .when().delete("/themes/1") - .then().log().all() - .statusCode(204); - } - - /* - * reservationData DataSet ThemeID 별 reservation 개수 - * 5,4,2,5,2,3,1,1,1,1,1 - * 예약 수 내림차순 + ThemeId 오름차순 정렬 순서 - * 1, 4, 2, 6, 3, 5, 7, 8, 9, 10 - */ - @Test - @DisplayName("예약 수 상위 10개 테마를 조회했을 때 내림차순으로 정렬된다. 만약 예약 수가 같다면, id 순으로 오름차순 정렬된다.") - @Sql(scripts = {"/truncate.sql", "/reservationData.sql"}, executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) - void readTop10ThemesDescOrder() { - RestAssured.given().log().all() - .contentType(ContentType.JSON) - .port(port) - .when().get("/themes/most-reserved-last-week?count=10") - .then().log().all() - .statusCode(200) - .body("data.themes.size()", is(10)) - .body("data.themes.id", contains(1, 4, 2, 6, 3, 5, 7, 8, 9, 10)); - } - - @ParameterizedTest - @MethodSource("requestValidateSource") - @DisplayName("테마 생성 시, 요청 값에 공백 또는 null이 포함되어 있으면 400 에러를 발생한다.") - void validateBlankRequest(Map invalidRequestBody) { - String email = "admin@test.com"; - String password = "12341234"; - String adminAccessTokenCookie = getAdminAccessTokenCookieByLogin(email, password); - - RestAssured.given().log().all() - .contentType(ContentType.JSON) - .header(new Header("Cookie", adminAccessTokenCookie)) - .port(port) - .body(invalidRequestBody) - .when().post("/themes") - .then().log().all() - .statusCode(400); - } - - static Stream> requestValidateSource() { - return Stream.of( - Map.of( - "name", "테마명", - "thumbnail", "http://testsfasdgasd.com" - ), - Map.of( - "name", "", - "description", "설명", - "thumbnail", "http://testsfasdgasd.com" - ), - Map.of( - "name", " ", - "description", "설명", - "thumbnail", "http://testsfasdgasd.com" - ) - ); - } - - private String getAdminAccessTokenCookieByLogin(final String email, final String password) { - memberRepository.save(new Member(null, "이름", email, password, Role.ADMIN)); - - Map loginParams = Map.of( - "email", email, - "password", password - ); - - String accessToken = RestAssured.given().log().all() - .contentType(ContentType.JSON) - .port(port) - .body(loginParams) - .when().post("/login") - .then().log().all().extract().cookie("accessToken"); - - return "accessToken=" + accessToken; - } -} diff --git a/src/test/java/roomescape/theme/infrastructure/persistence/ThemeRepositoryTest.kt b/src/test/java/roomescape/theme/infrastructure/persistence/ThemeRepositoryTest.kt new file mode 100644 index 00000000..73df4b11 --- /dev/null +++ b/src/test/java/roomescape/theme/infrastructure/persistence/ThemeRepositoryTest.kt @@ -0,0 +1,147 @@ +package roomescape.theme.infrastructure.persistence + +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldContainInOrder +import io.kotest.matchers.shouldBe +import jakarta.persistence.EntityManager +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import roomescape.theme.util.TestThemeCreateUtil +import java.time.LocalDate + +@DataJpaTest +class ThemeRepositoryTest( + val themeRepository: ThemeRepository, + val entityManager: EntityManager +) : FunSpec() { + + init { + context("findTopNThemeBetweenStartDateAndEndDate") { + beforeTest { + for (i in 1..10) { + TestThemeCreateUtil.createThemeWithReservations( + entityManager = entityManager, + name = "테마$i", + reservedCount = i, + date = LocalDate.now().minusDays(i.toLong()), + ) + } + } + + test("지난 10일간 예약 수가 가장 많은 테마 5개를 조회한다.") { + themeRepository.findTopNThemeBetweenStartDateAndEndDate( + LocalDate.now().minusDays(10), + LocalDate.now().minusDays(1), + 5 + ).also { themes -> + themes.size shouldBe 5 + themes.map { it.name } shouldContainInOrder listOf( + "테마10", "테마9", "테마8", "테마7", "테마6" + ) + } + } + + test("8일 전부터 5일 전까지 예약 수가 가장 많은 테마 3개를 조회한다.") { + themeRepository.findTopNThemeBetweenStartDateAndEndDate( + LocalDate.now().minusDays(8), + LocalDate.now().minusDays(5), + 3 + ).also { themes -> + themes.size shouldBe 3 + themes.map { it.name } shouldContainInOrder listOf( + "테마8", "테마7", "테마6" + ) + } + } + + test("예약 수가 동일하면 먼저 생성된 테마를 우선 조회한다.") { + TestThemeCreateUtil.createThemeWithReservations( + entityManager = entityManager, + name = "테마11", + reservedCount = 5, + date = LocalDate.now().minusDays(5), + ) + + themeRepository.findTopNThemeBetweenStartDateAndEndDate( + LocalDate.now().minusDays(6), + LocalDate.now().minusDays(4), + 5 + ).also { themes -> + themes.size shouldBe 4 + themes.map { it.name } shouldContainInOrder listOf( + "테마6", "테마5", "테마11", "테마4" + ) + } + } + + test("입력된 갯수보다 조회된 갯수가 작으면, 조회된 갯수만큼 반환한다.") { + themeRepository.findTopNThemeBetweenStartDateAndEndDate( + LocalDate.now().minusDays(10), + LocalDate.now().minusDays(6), + 10 + ).also { themes -> + themes.size shouldBe 5 + } + } + + test("입력된 갯수보다 조회된 갯수가 많으면, 입력된 갯수만큼 반환한다.") { + themeRepository.findTopNThemeBetweenStartDateAndEndDate( + LocalDate.now().minusDays(10), + LocalDate.now().minusDays(1), + 15 + ).also { themes -> + themes.size shouldBe 10 + } + } + + test("입력된 날짜 범위에 예약된 테마가 없을 경우 빈 리스트를 반환한다.") { + themeRepository.findTopNThemeBetweenStartDateAndEndDate( + LocalDate.now().plusDays(1), + LocalDate.now().plusDays(10), + 5 + ).also { themes -> + themes.size shouldBe 0 + } + } + } + context("existsByName ") { + val themeName = "test-theme" + beforeTest { + TestThemeCreateUtil.createThemeWithReservations( + entityManager = entityManager, + name = themeName, + reservedCount = 0, + date = LocalDate.now() + ) + } + test("테마 이름이 존재하면 true를 반환한다.") { + themeRepository.existsByName(themeName) shouldBe true + } + + test("테마 이름이 존재하지 않으면 false를 반환한다.") { + themeRepository.existsByName(themeName.repeat(2)) shouldBe false + } + } + + context("isReservedTheme") { + test("테마가 예약 중이면 true를 반환한다.") { + val theme = TestThemeCreateUtil.createThemeWithReservations( + entityManager = entityManager, + name = "예약된 테마", + reservedCount = 1, + date = LocalDate.now() + ) + themeRepository.isReservedTheme(theme.id!!) shouldBe true + } + + test("테마가 예약 중이 아니면 false를 반환한다.") { + val theme = TestThemeCreateUtil.createThemeWithReservations( + entityManager = entityManager, + name = "예약되지 않은 테마", + reservedCount = 0, + date = LocalDate.now() + ) + themeRepository.isReservedTheme(theme.id!!) shouldBe false + } + } + } +} diff --git a/src/test/java/roomescape/theme/service/ThemeServiceTest.java b/src/test/java/roomescape/theme/service/ThemeServiceTest.java deleted file mode 100644 index 6a440389..00000000 --- a/src/test/java/roomescape/theme/service/ThemeServiceTest.java +++ /dev/null @@ -1,164 +0,0 @@ -package roomescape.theme.service; - -import static org.assertj.core.api.Assertions.*; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.List; - -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.orm.jpa.DataJpaTest; -import org.springframework.context.annotation.Import; -import org.springframework.test.context.jdbc.Sql; - -import roomescape.member.business.MemberService; -import roomescape.member.infrastructure.persistence.Member; -import roomescape.member.infrastructure.persistence.MemberRepository; -import roomescape.member.infrastructure.persistence.Role; -import roomescape.reservation.dto.request.ReservationRequest; -import roomescape.reservation.dto.request.ReservationTimeRequest; -import roomescape.reservation.dto.response.ReservationTimeResponse; -import roomescape.reservation.service.ReservationService; -import roomescape.reservation.service.ReservationTimeService; -import roomescape.common.exception.RoomescapeException; -import roomescape.theme.domain.Theme; -import roomescape.theme.domain.repository.ThemeRepository; -import roomescape.theme.dto.ThemeRequest; -import roomescape.theme.dto.ThemeResponse; -import roomescape.theme.dto.ThemesResponse; - -@DataJpaTest -@Import({ReservationTimeService.class, ReservationService.class, MemberService.class, ThemeService.class}) -@Sql(scripts = "/truncate.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) -class ThemeServiceTest { - - @Autowired - private ThemeRepository themeRepository; - - @Autowired - private ThemeService themeService; - - @Autowired - private ReservationTimeService reservationTimeService; - - @Autowired - private MemberRepository memberRepository; - - @Autowired - private ReservationService reservationService; - - @Test - @DisplayName("테마를 조회한다.") - void findThemeById() { - // given - Theme theme = themeRepository.save(new Theme("name", "description", "thumbnail")); - - // when - Theme foundTheme = themeService.findThemeById(theme.getId()); - - // then - assertThat(foundTheme).isEqualTo(theme); - } - - @Test - @DisplayName("존재하지 않는 ID로 테마를 조회하면 예외가 발생한다.") - void findThemeByNotExistId() { - // given - Theme theme = themeRepository.save(new Theme("name", "description", "thumbnail")); - - // when - Long notExistId = theme.getId() + 1; - - // then - assertThatThrownBy(() -> themeService.findThemeById(notExistId)) - .isInstanceOf(RoomescapeException.class); - } - - @Test - @DisplayName("모든 테마를 조회한다.") - void findAllThemes() { - // given - Theme theme = themeRepository.save(new Theme("name", "description", "thumbnail")); - Theme theme1 = themeRepository.save(new Theme("name1", "description1", "thumbnail1")); - - // when - ThemesResponse found = themeService.findAllThemes(); - - // then - assertThat(found.themes()).extracting("id").containsExactly(theme.getId(), theme1.getId()); - } - - @Test - @DisplayName("예약 수 상위 10개 테마를 조회했을 때 내림차순으로 정렬된다. 만약 예약 수가 같다면, id 순으로 오름차순 정렬된다.") - @Sql({"/truncate.sql", "/reservationData.sql"}) - void getMostReservedThemesByCount() { - // given - LocalDate today = LocalDate.now(); - - // when - List found = themeService.getMostReservedThemesByCount(10).themes(); - - // then : 11번 테마는 조회되지 않아야 한다. - assertThat(found).extracting("id").containsExactly(1L, 4L, 2L, 6L, 3L, 5L, 7L, 8L, 9L, 10L); - } - - @Test - @DisplayName("테마를 추가한다.") - void addTheme() { - // given - ThemeResponse themeResponse = themeService.addTheme(new ThemeRequest("name", "description", "thumbnail")); - - // when - Theme found = themeRepository.findById(themeResponse.id()).orElse(null); - - // then - assertThat(found).isNotNull(); - } - - @Test - @DisplayName("테마를 추가할 때 같은 이름의 테마가 존재하면 예외가 발생한다. ") - void addDuplicateTheme() { - // given - ThemeResponse themeResponse = themeService.addTheme(new ThemeRequest("name", "description", "thumbnail")); - - // when - ThemeRequest invalidRequest = new ThemeRequest(themeResponse.name(), "description", "thumbnail"); - - // then - assertThatThrownBy(() -> themeService.addTheme(invalidRequest)) - .isInstanceOf(RoomescapeException.class); - } - - @Test - @DisplayName("테마를 삭제한다.") - void removeThemeById() { - // given - Theme theme = themeRepository.save(new Theme("name", "description", "thumbnail")); - - // when - themeService.removeThemeById(theme.getId()); - - // then - assertThat(themeRepository.findById(theme.getId())).isEmpty(); - } - - @Test - @DisplayName("예약이 존재하는 테마를 삭제하면 예외가 발생한다.") - void removeReservedTheme() { - // given - LocalDateTime dateTime = LocalDateTime.now().plusDays(1); - ReservationTimeResponse time = reservationTimeService.addTime( - new ReservationTimeRequest(dateTime.toLocalTime())); - Theme theme = themeRepository.save(new Theme("name", "description", "thumbnail")); - Member member = memberRepository.save(new Member(null, "member", "password", "name", Role.MEMBER)); - reservationService.addReservation( - new ReservationRequest(dateTime.toLocalDate(), time.id(), theme.getId(), "paymentKey", "orderId", 1000L, - "NORMAL"), member.getId()); - - // when & then - assertThatThrownBy(() -> themeService.removeThemeById(theme.getId())) - .isInstanceOf(RoomescapeException.class); - } -} diff --git a/src/test/java/roomescape/theme/util/TestThemeCreateUtil.kt b/src/test/java/roomescape/theme/util/TestThemeCreateUtil.kt new file mode 100644 index 00000000..4111939a --- /dev/null +++ b/src/test/java/roomescape/theme/util/TestThemeCreateUtil.kt @@ -0,0 +1,44 @@ +package roomescape.theme.util + +import jakarta.persistence.EntityManager +import roomescape.member.infrastructure.persistence.Member +import roomescape.reservation.domain.ReservationStatus +import roomescape.reservation.domain.ReservationTime +import roomescape.theme.infrastructure.persistence.ThemeEntity +import roomescape.util.MemberFixture +import roomescape.util.ReservationFixture +import roomescape.util.ReservationTimeFixture +import roomescape.util.ThemeFixture +import java.time.LocalDate +import java.time.LocalTime + +object TestThemeCreateUtil { + fun createThemeWithReservations( + entityManager: EntityManager, + name: String, + reservedCount: Int, + date: LocalDate, + ): ThemeEntity { + val themeEntity: ThemeEntity = ThemeFixture.create(name = name).also { entityManager.persist(it) } + val member: Member = MemberFixture.create().also { entityManager.persist(it) } + + for (i in 1..reservedCount) { + val time: ReservationTime = ReservationTimeFixture.create( + startAt = LocalTime.now().plusMinutes(i.toLong()) + ).also { entityManager.persist(it) } + + ReservationFixture.create( + date = date, + themeEntity = themeEntity, + member = member, + reservationTime = time, + status = ReservationStatus.CONFIRMED + ).also { entityManager.persist(it) } + } + + entityManager.flush() + entityManager.clear() + + return themeEntity + } +} diff --git a/src/test/java/roomescape/theme/web/MostReservedThemeAPITest.kt b/src/test/java/roomescape/theme/web/MostReservedThemeAPITest.kt new file mode 100644 index 00000000..8931f6a2 --- /dev/null +++ b/src/test/java/roomescape/theme/web/MostReservedThemeAPITest.kt @@ -0,0 +1,113 @@ +package roomescape.theme.web + +import io.kotest.core.spec.style.FunSpec +import io.restassured.module.kotlin.extensions.Given +import io.restassured.module.kotlin.extensions.Then +import io.restassured.module.kotlin.extensions.When +import jakarta.persistence.EntityManager +import org.hamcrest.Matchers.equalTo +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.test.context.jdbc.Sql +import org.springframework.transaction.support.TransactionTemplate +import roomescape.theme.business.ThemeService +import roomescape.theme.util.TestThemeCreateUtil +import java.time.LocalDate +import kotlin.random.Random + +/** + * GET /themes/most-reserved-last-week API 테스트 + * 상세 테스트는 Repository 테스트에서 진행 + * 날짜 범위, 예약 수만 검증 + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Sql(value = ["/truncate.sql"], executionPhase = Sql.ExecutionPhase.AFTER_TEST_CLASS) +class MostReservedThemeAPITest( + @LocalServerPort val port: Int, + val themeService: ThemeService, + val transactionTemplate: TransactionTemplate, + val entityManager: EntityManager, +) : FunSpec() { + init { + beforeSpec { + transactionTemplate.executeWithoutResult { + // 지난 7일간 예약된 테마 10개 생성 + for (i in 1..10) { + TestThemeCreateUtil.createThemeWithReservations( + entityManager = entityManager, + name = "테마$i", + reservedCount = 1, + date = LocalDate.now().minusDays(Random.nextLong(1, 7)) + ) + } + + // 8일 전 예약된 테마 1개 생성 + TestThemeCreateUtil.createThemeWithReservations( + entityManager = entityManager, + name = "테마11", + reservedCount = 1, + date = LocalDate.now().minusDays(8) + ) + } + } + + context("가장 많이 예약된 테마를 조회할 때, ") { + val endpoint = "/themes/most-reserved-last-week" + test("갯수를 입력하지 않으면 10개를 반환한다.") { + Given { + port(port) + } When { + get(endpoint) + } Then { + log().all() + statusCode(200) + body("data.themes.size()", equalTo(10)) + } + } + + test("입력된 갯수가 조회된 갯수보다 크면 조회된 갯수만큼 반환한다.") { + val count = 15 + Given { + port(port) + } When { + param("count", count) + get("/themes/most-reserved-last-week") + } Then { + log().all() + statusCode(200) + body("data.themes.size()", equalTo(10)) + } + } + + test("입력된 갯수가 조회된 갯수보다 작으면 입력된 갯수만큼 반환한다.") { + val count = 5 + Given { + port(port) + } When { + param("count", count) + get("/themes/most-reserved-last-week") + } Then { + log().all() + statusCode(200) + body("data.themes.size()", equalTo(count)) + } + } + + test("7일 전 부터 1일 전 까지 예약된 테마를 대상으로 한다.") { + // 현재 저장된 데이터는 지난 7일간 예약된 테마 10개와 8일 전 예약된 테마 1개 + // 8일 전 예약된 테마는 제외되어야 하므로, 10개가 조회되어야 한다. + val count = 11 + Given { + port(port) + } When { + param("count", count) + get("/themes/most-reserved-last-week") + } Then { + log().all() + statusCode(200) + body("data.themes.size()", equalTo(10)) + } + } + } + } +} diff --git a/src/test/java/roomescape/theme/web/ThemeControllerTest.kt b/src/test/java/roomescape/theme/web/ThemeControllerTest.kt new file mode 100644 index 00000000..af3ea616 --- /dev/null +++ b/src/test/java/roomescape/theme/web/ThemeControllerTest.kt @@ -0,0 +1,307 @@ +package roomescape.theme.web + +import com.ninjasquad.springmockk.MockkBean +import com.ninjasquad.springmockk.SpykBean +import io.kotest.assertions.assertSoftly +import io.kotest.matchers.collections.shouldContainAll +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.just +import io.mockk.runs +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.MockMvc +import roomescape.theme.business.ThemeService +import roomescape.theme.infrastructure.persistence.ThemeRepository +import roomescape.util.RoomescapeApiTest +import roomescape.util.ThemeFixture + +@WebMvcTest(ThemeController::class) +class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { + + @SpykBean + private lateinit var themeService: ThemeService + + @MockkBean + private lateinit var themeRepository: ThemeRepository + + init { + Given("모든 테마를 조회할 때") { + val endpoint = "/themes" + + When("로그인 상태가 아니라면") { + doNotLogin() + + Then("로그인 페이지로 이동한다.") { + runGetTest( + mockMvc = mockMvc, + endpoint = endpoint, + log = true + ) { + status { is3xxRedirection() } + header { + string("Location", "/login") + } + } + } + } + + When("로그인 상태라면") { + loginAsUser() + + Then("조회에 성공한다.") { + every { + themeRepository.findAll() + } returns listOf( + ThemeFixture.create(id = 1, name = "theme1"), + ThemeFixture.create(id = 2, name = "theme2"), + ThemeFixture.create(id = 3, name = "theme3") + ) + + val response: ThemesResponse = runGetTest( + mockMvc = mockMvc, + endpoint = endpoint, + log = true + ) { + status { isOk() } + content { + contentType(MediaType.APPLICATION_JSON) + } + }.andReturn().readValue(ThemesResponse::class.java) + + assertSoftly(response.themes) { + it.size shouldBe 3 + it.map { m -> m.name } shouldContainAll listOf("theme1", "theme2", "theme3") + } + } + } + } + + Given("테마를 추가할 때") { + val endpoint = "/themes" + val request = ThemeRequest( + name = "theme1", + description = "description1", + thumbnail = "http://example.com/thumbnail1.jpg" + ) + + When("로그인 상태가 아니라면") { + doNotLogin() + Then("로그인 페이지로 이동한다.") { + runPostTest( + mockMvc = mockMvc, + endpoint = endpoint, + body = request, + log = true + ) { + status { is3xxRedirection() } + header { + string("Location", "/login") + } + } + } + } + + When("관리자가 아닌 회원은") { + loginAsUser() + Then("로그인 페이지로 이동한다.") { + runPostTest( + mockMvc = mockMvc, + endpoint = endpoint, + body = request, + log = true + ) { + status { is3xxRedirection() } + jsonPath("$.errorType") { value("PERMISSION_DOES_NOT_EXIST") } + } + } + } + + When("동일한 이름의 테마가 있으면") { + loginAsAdmin() + + Then("409 에러를 응답한다.") { + every { + themeRepository.existsByName(request.name) + } returns true + + runPostTest( + mockMvc = mockMvc, + endpoint = endpoint, + body = request, + log = true + ) { + status { isConflict() } + jsonPath("$.errorType") { value("THEME_DUPLICATED") } + } + } + } + + When("값이 잘못 입력되면 400 에러를 응답한다") { + beforeTest { + loginAsAdmin() + } + + val request = ThemeRequest( + name = "theme1", + description = "description1", + thumbnail = "http://example.com/thumbnail1.jpg" + ) + + fun runTest(request: ThemeRequest) { + runPostTest( + mockMvc = mockMvc, + endpoint = endpoint, + body = request, + log = true + ) { + status { isBadRequest() } + } + } + + Then("이름이 공백인 경우") { + val invalidRequest = request.copy(name = " ") + runTest(invalidRequest) + } + + Then("이름이 20글자를 초과하는 경우") { + val invalidRequest = request.copy(name = "a".repeat(21)) + runTest(invalidRequest) + } + + Then("설명이 공백인 경우") { + val invalidRequest = request.copy(description = " ") + runTest(invalidRequest) + } + + Then("설명이 100글자를 초과하는 경우") { + val invalidRequest = request.copy(description = "a".repeat(101)) + runTest(invalidRequest) + } + + Then("썸네일이 공백인 경우") { + val invalidRequest = request.copy(thumbnail = " ") + runTest(invalidRequest) + } + + Then("썸네일이 URL 형식이 아닌 경우") { + val invalidRequest = request.copy(thumbnail = "invalid-url") + runTest(invalidRequest) + } + } + + When("저장에 성공하면") { + loginAsAdmin() + + val theme = ThemeFixture.create( + id = 1, + name = request.name, + description = request.description, + thumbnail = request.thumbnail + ) + + every { + themeRepository.existsByName(request.name) + } returns false + + every { + themeRepository.save(any()) + } returns theme + + Then("201 응답을 받는다.") { + runPostTest( + mockMvc = mockMvc, + endpoint = endpoint, + body = request, + log = true + ) { + status { isCreated() } + header { + string("Location", "/themes/${theme.id}") + } + jsonPath("$.data.id") { value(theme.id) } + jsonPath("$.data.name") { value(theme.name) } + jsonPath("$.data.description") { value(theme.description) } + jsonPath("$.data.thumbnail") { value(theme.thumbnail) } + } + } + } + } + + Given("테마를 제거할 때") { + val themeId = 1L + val endpoint = "/themes/$themeId" + + When("로그인 상태가 아니라면") { + doNotLogin() + Then("로그인 페이지로 이동한다.") { + runDeleteTest( + mockMvc = mockMvc, + endpoint = endpoint, + log = true + ) { + status { is3xxRedirection() } + header { + string("Location", "/login") + } + } + } + } + + When("관리자가 아닌 회원은") { + loginAsUser() + Then("로그인 페이지로 이동한다.") { + runDeleteTest( + mockMvc = mockMvc, + endpoint = endpoint, + log = true + ) { + status { is3xxRedirection() } + jsonPath("$.errorType") { value("PERMISSION_DOES_NOT_EXIST") } + } + } + } + + When("입력된 ID에 해당하는 테마가 없으면") { + loginAsAdmin() + + Then("409 에러를 응답한다.") { + every { + themeRepository.isReservedTheme(themeId) + } returns true + + runDeleteTest( + mockMvc = mockMvc, + endpoint = endpoint, + log = true + ) { + status { isConflict() } + jsonPath("$.errorType") { value("THEME_IS_USED_CONFLICT") } + } + } + } + + When("정상적으로 제거되면") { + loginAsAdmin() + + every { + themeRepository.isReservedTheme(themeId) + } returns false + + every { + themeRepository.deleteById(themeId) + } just runs + + Then("204 응답을 받는다.") { + runDeleteTest( + mockMvc = mockMvc, + endpoint = endpoint, + log = true + ) { + status { isNoContent() } + } + } + } + } + } +} diff --git a/src/test/java/roomescape/util/Fixtures.kt b/src/test/java/roomescape/util/Fixtures.kt index abe061ac..5236ef28 100644 --- a/src/test/java/roomescape/util/Fixtures.kt +++ b/src/test/java/roomescape/util/Fixtures.kt @@ -11,7 +11,7 @@ import roomescape.payment.web.PaymentCancel import roomescape.reservation.domain.Reservation import roomescape.reservation.domain.ReservationStatus import roomescape.reservation.domain.ReservationTime -import roomescape.theme.domain.Theme +import roomescape.theme.infrastructure.persistence.ThemeEntity import java.time.LocalDate import java.time.LocalTime import java.time.OffsetDateTime @@ -62,18 +62,18 @@ object ThemeFixture { name: String = "Default Theme", description: String = "Default Description", thumbnail: String = "https://example.com/default-thumbnail.jpg" - ): Theme = Theme(id, name, description, thumbnail) + ): ThemeEntity = ThemeEntity(id, name, description, thumbnail) } object ReservationFixture { fun create( id: Long? = null, date: LocalDate = LocalDate.now().plusWeeks(1), - theme: Theme = ThemeFixture.create(), + themeEntity: ThemeEntity = ThemeFixture.create(), reservationTime: ReservationTime = ReservationTimeFixture.create(), member: Member = MemberFixture.create(), status: ReservationStatus = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED - ): Reservation = Reservation(id, date, reservationTime, theme, member, status) + ): Reservation = Reservation(id, date, reservationTime, themeEntity, member, status) } object JwtFixture { diff --git a/src/test/java/roomescape/util/RoomescapeApiTest.kt b/src/test/java/roomescape/util/RoomescapeApiTest.kt index 23addfe6..803bf86e 100644 --- a/src/test/java/roomescape/util/RoomescapeApiTest.kt +++ b/src/test/java/roomescape/util/RoomescapeApiTest.kt @@ -55,9 +55,7 @@ abstract class RoomescapeApiTest : BehaviorSpec() { header(HttpHeaders.COOKIE, "accessToken=token") }.apply { log.takeIf { it }?.let { this.andDo { print() } } - }.andExpect { - assert - } + }.andExpect(assert) fun runPostTest( mockMvc: MockMvc, @@ -77,6 +75,19 @@ abstract class RoomescapeApiTest : BehaviorSpec() { assert } + fun runDeleteTest( + mockMvc: MockMvc, + endpoint: String, + log: Boolean = false, + assert: MockMvcResultMatchersDsl.() -> Unit + ): ResultActionsDsl = mockMvc.delete(endpoint) { + header(HttpHeaders.COOKIE, "accessToken=token") + }.apply { + log.takeIf { it }?.let { this.andDo { print() } } + }.andExpect { + assert + } + fun loginAsAdmin() { every { jwtHandler.getMemberIdFromToken(any()) @@ -104,6 +115,13 @@ abstract class RoomescapeApiTest : BehaviorSpec() { every { memberRepository.findByIdOrNull(NOT_LOGGED_IN_USERID) } returns null } + fun MvcResult.readValue(valueType: Class): T = this.response.contentAsString + .takeIf { it.isNotBlank() } + ?.let { readValue(it, valueType) } + ?: throw RuntimeException(""" + [Test] Exception occurred while reading response json: ${this.response.contentAsString} with value type: $valueType + """.trimIndent()) + fun readValue(responseJson: String, valueType: Class): T = objectMapper .readTree(responseJson)["data"] ?.let { objectMapper.convertValue(it, valueType) }