generated from pricelees/issue-pr-template
refactor: ThemeService 및 테스트 코틀린 전환
This commit is contained in:
parent
60f5fd6b00
commit
68de9179ad
@ -1,81 +1,74 @@
|
|||||||
package roomescape.theme.business;
|
package roomescape.theme.business
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
import java.util.List;
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.transaction.annotation.Transactional
|
||||||
import org.springframework.stereotype.Service;
|
import roomescape.common.exception.ErrorType
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import roomescape.common.exception.RoomescapeException
|
||||||
|
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||||
import roomescape.common.exception.ErrorType;
|
import roomescape.theme.infrastructure.persistence.ThemeRepository
|
||||||
import roomescape.common.exception.RoomescapeException;
|
import roomescape.theme.web.ThemeRequest
|
||||||
import roomescape.theme.infrastructure.persistence.ThemeEntity;
|
import roomescape.theme.web.ThemeResponse
|
||||||
import roomescape.theme.infrastructure.persistence.ThemeRepository;
|
import roomescape.theme.web.ThemesResponse
|
||||||
import roomescape.theme.web.ThemeRequest;
|
import roomescape.theme.web.toResponse
|
||||||
import roomescape.theme.web.ThemeResponse;
|
import java.time.LocalDate
|
||||||
import roomescape.theme.web.ThemesResponse;
|
|
||||||
|
|
||||||
@Service
|
@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
|
@Transactional
|
||||||
public class ThemeService {
|
fun save(request: ThemeRequest): ThemeResponse {
|
||||||
|
if (themeRepository.existsByName(request.name)) {
|
||||||
private final ThemeRepository themeRepository;
|
throw RoomescapeException(
|
||||||
|
ErrorType.THEME_DUPLICATED,
|
||||||
public ThemeService(ThemeRepository themeRepository) {
|
"[name: ${request.name}]",
|
||||||
this.themeRepository = themeRepository;
|
HttpStatus.CONFLICT
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
return ThemeEntity(
|
||||||
public ThemeEntity findThemeById(Long id) {
|
name = request.name,
|
||||||
return themeRepository.findById(id)
|
description = request.description,
|
||||||
.orElseThrow(() -> new RoomescapeException(ErrorType.THEME_NOT_FOUND,
|
thumbnail = request.thumbnail
|
||||||
String.format("[themeId: %d]", id), HttpStatus.BAD_REQUEST));
|
).also {
|
||||||
|
themeRepository.save(it)
|
||||||
|
}.toResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional
|
||||||
public ThemesResponse findAllThemes() {
|
fun deleteById(id: Long) {
|
||||||
List<ThemeResponse> 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<ThemeResponse> response = themeRepository.findTopNThemeBetweenStartDateAndEndDate(startDate, endDate,
|
|
||||||
count)
|
|
||||||
.stream()
|
|
||||||
.map(ThemeResponse::from)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
return new ThemesResponse(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ThemeResponse addTheme(ThemeRequest request) {
|
|
||||||
validateIsSameThemeNameExist(request.name());
|
|
||||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(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)) {
|
if (themeRepository.isReservedTheme(id)) {
|
||||||
throw new RoomescapeException(ErrorType.THEME_IS_USED_CONFLICT,
|
throw RoomescapeException(
|
||||||
String.format("[themeId: %d]", id), HttpStatus.CONFLICT);
|
ErrorType.THEME_IS_USED_CONFLICT,
|
||||||
|
"[themeId: %d]",
|
||||||
|
HttpStatus.CONFLICT
|
||||||
|
)
|
||||||
}
|
}
|
||||||
themeRepository.deleteById(id);
|
themeRepository.deleteById(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,7 +34,7 @@ class ThemeController(
|
|||||||
override fun saveTheme(
|
override fun saveTheme(
|
||||||
@RequestBody @Valid request: ThemeRequest
|
@RequestBody @Valid request: ThemeRequest
|
||||||
): ResponseEntity<CommonApiResponse<ThemeResponse>> {
|
): ResponseEntity<CommonApiResponse<ThemeResponse>> {
|
||||||
val themeResponse: ThemeResponse = themeService.addTheme(request)
|
val themeResponse: ThemeResponse = themeService.save(request)
|
||||||
|
|
||||||
return ResponseEntity.created(URI.create("/themes/${themeResponse.id}"))
|
return ResponseEntity.created(URI.create("/themes/${themeResponse.id}"))
|
||||||
.body(CommonApiResponse(themeResponse))
|
.body(CommonApiResponse(themeResponse))
|
||||||
@ -44,7 +44,7 @@ class ThemeController(
|
|||||||
override fun removeTheme(
|
override fun removeTheme(
|
||||||
@PathVariable id: Long
|
@PathVariable id: Long
|
||||||
): ResponseEntity<CommonApiResponse<Unit>> {
|
): ResponseEntity<CommonApiResponse<Unit>> {
|
||||||
themeService.removeThemeById(id)
|
themeService.deleteById(id)
|
||||||
|
|
||||||
return ResponseEntity.noContent().build()
|
return ResponseEntity.noContent().build()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,164 +1,103 @@
|
|||||||
package roomescape.theme.business;
|
package roomescape.theme.business
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.*;
|
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
|
||||||
|
|
||||||
import java.time.LocalDate;
|
class ThemeServiceTest : FunSpec({
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.DisplayName;
|
val themeRepository: ThemeRepository = mockk()
|
||||||
import org.junit.jupiter.api.Test;
|
val themeService = ThemeService(themeRepository)
|
||||||
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;
|
context("findThemeById") {
|
||||||
import roomescape.member.infrastructure.persistence.Member;
|
val themeId = 1L
|
||||||
import roomescape.member.infrastructure.persistence.MemberRepository;
|
test("조회 성공") {
|
||||||
import roomescape.member.infrastructure.persistence.Role;
|
val theme: ThemeEntity = ThemeFixture.create(id = themeId)
|
||||||
import roomescape.reservation.dto.request.ReservationRequest;
|
every {
|
||||||
import roomescape.reservation.dto.request.ReservationTimeRequest;
|
themeRepository.findByIdOrNull(themeId)
|
||||||
import roomescape.reservation.dto.response.ReservationTimeResponse;
|
} returns theme
|
||||||
import roomescape.reservation.service.ReservationService;
|
|
||||||
import roomescape.reservation.service.ReservationTimeService;
|
|
||||||
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;
|
|
||||||
|
|
||||||
@DataJpaTest
|
theme.id shouldBe themeId
|
||||||
@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
|
|
||||||
ThemeEntity theme = themeRepository.save(new ThemeEntity("name", "description", "thumbnail"));
|
|
||||||
|
|
||||||
// when
|
|
||||||
ThemeEntity foundTheme = themeService.findThemeById(theme.getId());
|
|
||||||
|
|
||||||
// then
|
|
||||||
assertThat(foundTheme).isEqualTo(theme);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
test("ID로 테마를 찾을 수 없으면 400 예외를 던진다.") {
|
||||||
@DisplayName("존재하지 않는 ID로 테마를 조회하면 예외가 발생한다.")
|
every {
|
||||||
void findThemeByNotExistId() {
|
themeRepository.findByIdOrNull(themeId)
|
||||||
// given
|
} returns null
|
||||||
ThemeEntity theme = themeRepository.save(new ThemeEntity("name", "description", "thumbnail"));
|
|
||||||
|
|
||||||
// when
|
val exception = shouldThrow<RoomescapeException> {
|
||||||
Long notExistId = theme.getId() + 1;
|
themeService.findThemeById(themeId)
|
||||||
|
|
||||||
// then
|
|
||||||
assertThatThrownBy(() -> themeService.findThemeById(notExistId))
|
|
||||||
.isInstanceOf(RoomescapeException.class);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
exception.errorType shouldBe ErrorType.THEME_NOT_FOUND
|
||||||
@DisplayName("모든 테마를 조회한다.")
|
|
||||||
void findAllThemes() {
|
|
||||||
// given
|
|
||||||
ThemeEntity theme = themeRepository.save(new ThemeEntity("name", "description", "thumbnail"));
|
|
||||||
ThemeEntity theme1 = themeRepository.save(new ThemeEntity("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<ThemeResponse> 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
|
|
||||||
ThemeEntity 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
|
|
||||||
ThemeEntity theme = themeRepository.save(new ThemeEntity("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()));
|
|
||||||
ThemeEntity theme = themeRepository.save(new ThemeEntity("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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<RoomescapeException> {
|
||||||
|
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<RoomescapeException> {
|
||||||
|
themeService.deleteById(themeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertSoftly(exception) {
|
||||||
|
this.errorType shouldBe ErrorType.THEME_IS_USED_CONFLICT
|
||||||
|
this.httpStatus shouldBe HttpStatus.CONFLICT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user