Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

이용 가능한 쿠폰 목록 조회 API 구현 #46

Merged
merged 1 commit into from
Aug 20, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
feat: #45 이용 가능한 쿠폰 목록 조회 API 구현
  • Loading branch information
Wo-ogie committed Aug 20, 2024
commit 0c9dee3830ec8d404b1448232bc7c4905aca02b2
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.routebox.routebox.application.coupon

import com.routebox.routebox.domain.coupon.Coupon
import com.routebox.routebox.domain.coupon.CouponService
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional

@Component
class FindAvailableCouponsUseCase(private val couponService: CouponService) {
@Transactional(readOnly = true)
operator fun invoke(userId: Long): List<Coupon> =
couponService.findAvailableCoupons(userId)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.routebox.routebox.controller.coupon

import com.routebox.routebox.application.coupon.FindAvailableCouponsUseCase
import com.routebox.routebox.controller.coupon.dto.FindAvailableCouponsResponse
import com.routebox.routebox.security.UserPrincipal
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@Tag(name = "쿠폰 관련 API")
@RequestMapping("/api")
@RestController
class CouponController(
private val findAvailableCouponsUseCase: FindAvailableCouponsUseCase,
) {
@Operation(
summary = "이용 가능한 쿠폰 목록 조회",
description = "현재 이용 가능한 쿠폰 목록을 조회합니다.",
security = [SecurityRequirement(name = "access-token")],
)
@GetMapping("/v1/coupons/available")
fun findAvailableCoupons(
@AuthenticationPrincipal userPrincipal: UserPrincipal,
): FindAvailableCouponsResponse {
val availableCoupons = findAvailableCouponsUseCase(userId = userPrincipal.userId)
return FindAvailableCouponsResponse.from(availableCoupons)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.routebox.routebox.controller.coupon.dto

import com.routebox.routebox.domain.coupon.Coupon
import com.routebox.routebox.domain.coupon.constant.CouponStatus
import com.routebox.routebox.domain.coupon.constant.CouponType
import io.swagger.v3.oas.annotations.media.Schema
import java.time.LocalDateTime

data class FindAvailableCouponsResponse(
val coupons: List<CouponResponse>,
) {
companion object {
fun from(coupons: List<Coupon>): FindAvailableCouponsResponse {
val couponResponses = coupons.map { couponEntity -> CouponResponse.from(couponEntity) }
return FindAvailableCouponsResponse(couponResponses)
}
}

data class CouponResponse(
@Schema(description = "Id of coupon", example = "13")
val id: Long,

@Schema(description = "쿠폰 제목", example = "회원가입 기념 쿠폰")
val title: String,

@Schema(description = "쿠폰 종류")
val type: CouponType,

@Schema(description = "쿠폰 상태")
val status: CouponStatus,

@Schema(description = "쿠폰 이용이 가능한 시작 시각")
val startedAt: LocalDateTime,

@Schema(description = "쿠폰 이용이 가능한 종료 시각")
val endedAt: LocalDateTime?,

@Schema(description = "쿠폰이 만료된 시각")
val expiredAt: LocalDateTime?,
) {
companion object {
fun from(coupon: Coupon): CouponResponse =
CouponResponse(
id = coupon.id,
title = coupon.title,
type = coupon.type,
status = coupon.status,
startedAt = coupon.startedAt,
endedAt = coupon.endedAt,
expiredAt = coupon.expiredAt,
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.routebox.routebox.domain.coupon

import com.routebox.routebox.domain.coupon.constant.CouponStatus
import com.routebox.routebox.infrastructure.coupon.CouponRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
class CouponService(private val couponRepository: CouponRepository) {

@Transactional(readOnly = true)
fun findAvailableCoupons(userId: Long): List<Coupon> =
couponRepository.findByUserIdAndStatus(userId, CouponStatus.AVAILABLE)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.routebox.routebox.infrastructure.coupon

import com.routebox.routebox.domain.coupon.Coupon
import com.routebox.routebox.domain.coupon.constant.CouponStatus
import org.springframework.data.jpa.repository.JpaRepository

interface CouponRepository : JpaRepository<Coupon, Long>
interface CouponRepository : JpaRepository<Coupon, Long> {
fun findByUserIdAndStatus(userId: Long, status: CouponStatus): List<Coupon>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.routebox.routebox.application.coupon

import com.routebox.routebox.domain.coupon.Coupon
import com.routebox.routebox.domain.coupon.CouponService
import com.routebox.routebox.domain.coupon.constant.CouponStatus
import com.routebox.routebox.domain.coupon.constant.CouponType
import org.apache.commons.lang3.RandomStringUtils
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.given
import org.mockito.kotlin.then
import java.time.LocalDateTime
import kotlin.random.Random
import kotlin.test.Test

@ExtendWith(MockitoExtension::class)
class FindAvailableCouponsUseCaseTest {

@InjectMocks
private lateinit var sut: FindAvailableCouponsUseCase

@Mock
private lateinit var couponService: CouponService

@Test
fun `이용 가능한 쿠폰을 조회하면, 상태가 AVAILABLE인 쿠폰 목록이 반환된다`() {
// given
val userId = Random.nextLong()
val expectedResult = listOf(createCoupon(userId = Random.nextLong()))
given(couponService.findAvailableCoupons(userId)).willReturn(expectedResult)

// when
val actualResult = sut.invoke(userId)

// then
then(couponService).should().findAvailableCoupons(userId)
verifyEveryMocksShouldHaveNoMoreInteractions()
Assertions.assertThatIterable(actualResult).isEqualTo(expectedResult)
}

private fun verifyEveryMocksShouldHaveNoMoreInteractions() {
then(couponService).shouldHaveNoMoreInteractions()
}

private fun createCoupon(userId: Long): Coupon = Coupon(
id = Random.nextLong(),
userId = userId,
title = RandomStringUtils.random(10),
type = CouponType.BUY_ROUTE,
status = CouponStatus.AVAILABLE,
startedAt = LocalDateTime.now(),
endedAt = LocalDateTime.now(),
expiredAt = LocalDateTime.now(),
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.routebox.routebox.controller.coupon

import com.routebox.routebox.application.coupon.FindAvailableCouponsUseCase
import com.routebox.routebox.config.ControllerTestConfig
import com.routebox.routebox.domain.coupon.Coupon
import com.routebox.routebox.domain.coupon.constant.CouponStatus
import com.routebox.routebox.domain.coupon.constant.CouponType
import com.routebox.routebox.domain.user.constant.UserRoleType
import com.routebox.routebox.security.UserPrincipal
import org.apache.commons.lang3.RandomStringUtils
import org.mockito.BDDMockito.given
import org.mockito.BDDMockito.then
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.context.annotation.Import
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import java.time.LocalDateTime
import kotlin.random.Random
import kotlin.test.Test

@Import(ControllerTestConfig::class)
@WebMvcTest(controllers = [CouponController::class])
class CouponControllerTest @Autowired constructor(private val mvc: MockMvc) {

@MockBean
lateinit var findAvailableCouponsUseCase: FindAvailableCouponsUseCase

@Test
fun `이용 가능한 쿠폰을 조회하면, 상태가 AVAILABLE인 쿠폰 목록이 반환된다`() {
// given
val userId = Random.nextLong()
val expectedResult = listOf(createCoupon(userId))
given(findAvailableCouponsUseCase.invoke(userId)).willReturn(expectedResult)

// when & then
mvc.perform(
get("/api/v1/coupons/available")
.with(user(createUserPrincipal(userId))),
).andExpect(status().isOk)
.andExpect(jsonPath("$.coupons.size()").value(expectedResult.size))
then(findAvailableCouponsUseCase).should().invoke(userId)
verifyEveryMocksShouldHaveNoMoreInteractions()
}

private fun verifyEveryMocksShouldHaveNoMoreInteractions() {
then(findAvailableCouponsUseCase).shouldHaveNoMoreInteractions()
}

private fun createCoupon(userId: Long): Coupon = Coupon(
id = Random.nextLong(),
userId = userId,
title = RandomStringUtils.random(10),
type = CouponType.BUY_ROUTE,
status = CouponStatus.AVAILABLE,
startedAt = LocalDateTime.now(),
endedAt = LocalDateTime.now(),
expiredAt = LocalDateTime.now(),
)

private fun createUserPrincipal(userId: Long) = UserPrincipal(
userId = userId,
socialLoginUid = userId.toString(),
userRoles = setOf(UserRoleType.USER),
)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.routebox.routebox.controller.user

import com.fasterxml.jackson.databind.ObjectMapper
import com.routebox.routebox.application.user.CheckNicknameAvailabilityUseCase
import com.routebox.routebox.application.user.GetUserProfileUseCase
import com.routebox.routebox.application.user.UpdateUserInfoUseCase
@@ -33,10 +32,7 @@ import kotlin.test.Test

@Import(ControllerTestConfig::class)
@WebMvcTest(controllers = [UserController::class])
class UserControllerTest @Autowired constructor(
private val mvc: MockMvc,
private val mapper: ObjectMapper,
) {
class UserControllerTest @Autowired constructor(private val mvc: MockMvc) {
@MockBean
lateinit var getUserProfileUseCase: GetUserProfileUseCase

Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.routebox.routebox.domain.coupon

import com.routebox.routebox.domain.coupon.constant.CouponStatus
import com.routebox.routebox.domain.coupon.constant.CouponType
import com.routebox.routebox.infrastructure.coupon.CouponRepository
import org.apache.commons.lang3.RandomStringUtils
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.given
import org.mockito.kotlin.then
import java.time.LocalDateTime
import kotlin.random.Random
import kotlin.test.Test

@ExtendWith(MockitoExtension::class)
class CouponServiceTest {

@InjectMocks
private lateinit var sut: CouponService

@Mock
private lateinit var couponRepository: CouponRepository

@Test
fun `이용 가능한 쿠폰을 조회하면, 상태가 AVAILABLE인 쿠폰 목록이 반환된다`() {
// given
val userId = Random.nextLong()
val expectedResult = listOf(createCoupon(userId = Random.nextLong()))
given(couponRepository.findByUserIdAndStatus(userId, CouponStatus.AVAILABLE)).willReturn(expectedResult)

// when
val actualResult = sut.findAvailableCoupons(userId)

// then
then(couponRepository).should().findByUserIdAndStatus(userId, CouponStatus.AVAILABLE)
verifyEveryMocksShouldHaveNoMoreInteractions()
Assertions.assertThatIterable(actualResult).isEqualTo(expectedResult)
}

private fun verifyEveryMocksShouldHaveNoMoreInteractions() {
then(couponRepository).shouldHaveNoMoreInteractions()
}

private fun createCoupon(userId: Long): Coupon = Coupon(
id = Random.nextLong(),
userId = userId,
title = RandomStringUtils.random(10),
type = CouponType.BUY_ROUTE,
status = CouponStatus.AVAILABLE,
startedAt = LocalDateTime.now(),
endedAt = LocalDateTime.now(),
expiredAt = LocalDateTime.now(),
)
}
Loading