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 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.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 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
|
||||
public class ThemeService {
|
||||
|
||||
private final ThemeRepository themeRepository;
|
||||
|
||||
public ThemeService(ThemeRepository themeRepository) {
|
||||
this.themeRepository = themeRepository;
|
||||
fun save(request: ThemeRequest): ThemeResponse {
|
||||
if (themeRepository.existsByName(request.name)) {
|
||||
throw RoomescapeException(
|
||||
ErrorType.THEME_DUPLICATED,
|
||||
"[name: ${request.name}]",
|
||||
HttpStatus.CONFLICT
|
||||
)
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ThemeEntity findThemeById(Long id) {
|
||||
return themeRepository.findById(id)
|
||||
.orElseThrow(() -> new RoomescapeException(ErrorType.THEME_NOT_FOUND,
|
||||
String.format("[themeId: %d]", id), HttpStatus.BAD_REQUEST));
|
||||
return ThemeEntity(
|
||||
name = request.name,
|
||||
description = request.description,
|
||||
thumbnail = request.thumbnail
|
||||
).also {
|
||||
themeRepository.save(it)
|
||||
}.toResponse()
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ThemesResponse findAllThemes() {
|
||||
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) {
|
||||
@Transactional
|
||||
fun deleteById(id: Long) {
|
||||
if (themeRepository.isReservedTheme(id)) {
|
||||
throw new RoomescapeException(ErrorType.THEME_IS_USED_CONFLICT,
|
||||
String.format("[themeId: %d]", id), HttpStatus.CONFLICT);
|
||||
throw RoomescapeException(
|
||||
ErrorType.THEME_IS_USED_CONFLICT,
|
||||
"[themeId: %d]",
|
||||
HttpStatus.CONFLICT
|
||||
)
|
||||
}
|
||||
themeRepository.deleteById(id);
|
||||
themeRepository.deleteById(id)
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,7 +34,7 @@ class ThemeController(
|
||||
override fun saveTheme(
|
||||
@RequestBody @Valid request: ThemeRequest
|
||||
): ResponseEntity<CommonApiResponse<ThemeResponse>> {
|
||||
val themeResponse: ThemeResponse = themeService.addTheme(request)
|
||||
val themeResponse: ThemeResponse = themeService.save(request)
|
||||
|
||||
return ResponseEntity.created(URI.create("/themes/${themeResponse.id}"))
|
||||
.body(CommonApiResponse(themeResponse))
|
||||
@ -44,7 +44,7 @@ class ThemeController(
|
||||
override fun removeTheme(
|
||||
@PathVariable id: Long
|
||||
): ResponseEntity<CommonApiResponse<Unit>> {
|
||||
themeService.removeThemeById(id)
|
||||
themeService.deleteById(id)
|
||||
|
||||
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;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
class ThemeServiceTest : FunSpec({
|
||||
|
||||
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;
|
||||
val themeRepository: ThemeRepository = mockk()
|
||||
val themeService = ThemeService(themeRepository)
|
||||
|
||||
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.infrastructure.persistence.ThemeEntity;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeRepository;
|
||||
import roomescape.theme.web.ThemeRequest;
|
||||
import roomescape.theme.web.ThemeResponse;
|
||||
import roomescape.theme.web.ThemesResponse;
|
||||
context("findThemeById") {
|
||||
val themeId = 1L
|
||||
test("조회 성공") {
|
||||
val theme: ThemeEntity = ThemeFixture.create(id = themeId)
|
||||
every {
|
||||
themeRepository.findByIdOrNull(themeId)
|
||||
} returns theme
|
||||
|
||||
@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
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity("name", "description", "thumbnail"));
|
||||
|
||||
// when
|
||||
ThemeEntity foundTheme = themeService.findThemeById(theme.getId());
|
||||
|
||||
// then
|
||||
assertThat(foundTheme).isEqualTo(theme);
|
||||
theme.id shouldBe themeId
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("존재하지 않는 ID로 테마를 조회하면 예외가 발생한다.")
|
||||
void findThemeByNotExistId() {
|
||||
// given
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity("name", "description", "thumbnail"));
|
||||
test("ID로 테마를 찾을 수 없으면 400 예외를 던진다.") {
|
||||
every {
|
||||
themeRepository.findByIdOrNull(themeId)
|
||||
} returns null
|
||||
|
||||
// when
|
||||
Long notExistId = theme.getId() + 1;
|
||||
|
||||
// then
|
||||
assertThatThrownBy(() -> themeService.findThemeById(notExistId))
|
||||
.isInstanceOf(RoomescapeException.class);
|
||||
val exception = shouldThrow<RoomescapeException> {
|
||||
themeService.findThemeById(themeId)
|
||||
}
|
||||
|
||||
@Test
|
||||
@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);
|
||||
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<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