Test #460

Merged
klaus merged 8 commits from test into main 2026-10-26 04:58:47 +00:00
8 changed files with 312 additions and 20 deletions
Showing only changes of commit f46a6c4c7c - Show all commits
@@ -128,3 +128,11 @@ CREATE TABLE gift_application_no_sequence (
) COMMENT='선물 신청번호 날짜별 채번';
ALTER TABLE gift_category DROP INDEX uk_gift_category_receipt_code;
-- 2026-10-23: 전달 완료 후 배송정보 삭제를 위한 발신자 컬럼 NULL 허용.
-- 기존 DB에는 아래 ALTER TABLE 문만 적용하며, 이 파일 전체를 재실행하지 않는다.
ALTER TABLE gift_delivery
MODIFY COLUMN sender_name VARCHAR(50) NULL COMMENT '발신자 이름',
MODIFY COLUMN sender_phone_number VARCHAR(30) NULL COMMENT '발신자 휴대폰 번호',
MODIFY COLUMN sender_zip_code VARCHAR(20) NULL COMMENT '발신자 우편번호',
MODIFY COLUMN sender_address VARCHAR(255) NULL COMMENT '발신자 주소';
@@ -16,20 +16,20 @@ class GiftDelivery(
@Column(name = "gift_id", nullable = false)
val giftId: Long,
@Column(name = "sender_name", nullable = false, length = 50)
val senderName: String,
@Column(name = "sender_name", length = 50)
var senderName: String?,
@Column(name = "sender_phone_number", nullable = false, length = 30)
val senderPhoneNumber: String,
@Column(name = "sender_phone_number", length = 30)
var senderPhoneNumber: String?,
@Column(name = "sender_zip_code", nullable = false, length = 20)
val senderZipCode: String,
@Column(name = "sender_zip_code", length = 20)
var senderZipCode: String?,
@Column(name = "sender_address", nullable = false, length = 255)
val senderAddress: String,
@Column(name = "sender_address", length = 255)
var senderAddress: String?,
@Column(name = "sender_address_detail", length = 255)
val senderAddressDetail: String? = null,
var senderAddressDetail: String? = null,
@Column(name = "recipient_name", length = 50)
var recipientName: String? = null,
@@ -115,8 +115,8 @@ class GiftAdminQueryService(
applicationNo = gift.applicationNo,
senderInfo = AdminGiftMemberInfoResult(
nickname = sender.nickname,
name = delivery.senderName,
phoneNumber = delivery.senderPhoneNumber,
name = delivery.senderName.orEmpty(),
phoneNumber = delivery.senderPhoneNumber.orEmpty(),
address = formatAddress(delivery.senderZipCode, delivery.senderAddress, delivery.senderAddressDetail)
),
recipientInfo = AdminGiftMemberInfoResult(
@@ -174,8 +174,8 @@ class GiftAdminQueryService(
return formatAddress(zipCode, address, recipientAddressDetail)
}
private fun formatAddress(zipCode: String, address: String, detail: String?) =
"($zipCode) $address" + if (detail.isNullOrBlank()) "" else ", $detail"
private fun formatAddress(zipCode: String?, address: String?, detail: String?) =
if (zipCode == null || address == null) "" else "($zipCode) $address" + if (detail.isNullOrBlank()) "" else ", $detail"
private fun invalidRequest() = SodaException(messageKey = "common.error.invalid_request")
@@ -259,8 +259,8 @@ class GiftQueryService(
}
private fun GiftDelivery.toSenderInfo() = GiftAddressResult(
name = senderName,
phoneNumber = senderPhoneNumber,
name = senderName.orEmpty(),
phoneNumber = senderPhoneNumber.orEmpty(),
address = formatAddress(senderZipCode, senderAddress, senderAddressDetail)
)
@@ -305,8 +305,8 @@ class GiftQueryService(
)
}
private fun formatAddress(zipCode: String, address: String, detail: String?) =
"($zipCode) $address" + if (detail.isNullOrBlank()) "" else ", $detail"
private fun formatAddress(zipCode: String?, address: String?, detail: String?) =
if (zipCode == null || address == null) "" else "($zipCode) $address" + if (detail.isNullOrBlank()) "" else ", $detail"
private fun invalidRequest() = SodaException(messageKey = "common.error.invalid_request")
@@ -29,8 +29,11 @@ import kr.co.vividnext.sodalive.v2.gift.application.GiftTrackingRegistrationResu
import kr.co.vividnext.sodalive.v2.gift.domain.GiftSize
import kr.co.vividnext.sodalive.v2.gift.domain.GiftStatus
import org.hamcrest.Matchers.endsWith
import org.hamcrest.Matchers.nullValue
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
import org.mockito.Mockito
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
@@ -219,6 +222,84 @@ class GiftControllerTest @Autowired constructor(
.andExpect(jsonPath("$.data.statusTimeline[1].occurredAt").value("2026-09-30T12:00:00Z"))
}
@Test
@DisplayName("배송 개인정보가 지워져도 보낸 선물과 받은 선물의 상세 응답 계약을 유지한다")
fun shouldReturnCompatibleErasedGiftDetailsForSenderAndRecipient() {
val sender = memberRepository.save(Member(password = "password", nickname = "erased-fan"))
val recipient = memberRepository.save(Member(password = "password", nickname = "erased-creator"))
val gift = saveGift("G-CONTROLLER-ERASED", sender.id!!, recipient.id!!, GiftStatus.DELIVERED)
val delivery = GiftDelivery(
giftId = gift.id!!,
senderName = null,
senderPhoneNumber = null,
senderZipCode = null,
senderAddress = null,
trackingDeadlineAt = LocalDateTime.of(2026, 10, 2, 12, 0),
recipientAddressDeadlineAt = LocalDateTime.of(2026, 10, 7, 12, 0),
deliveredAt = LocalDateTime.of(2026, 10, 3, 12, 0)
)
giftDeliveryRepository.saveAndFlush(delivery)
mockMvc.perform(get("/api/v2/gifts/{applicationNo}", gift.applicationNo).with(user(MemberAdapter(sender))))
.andExpect(status().isOk)
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.direction").value("SENT"))
.andExpect(jsonPath("$.data.status").value("DELIVERED"))
.andExpect(jsonPath("$.data.senderInfo").isMap)
.andExpect(jsonPath("$.data.senderInfo.name").value(""))
.andExpect(jsonPath("$.data.senderInfo.phoneNumber").value(""))
.andExpect(jsonPath("$.data.senderInfo.address").value(""))
.andExpect(jsonPath("$.data.recipientAddress").value(nullValue()))
.andExpect(jsonPath("$.data.giftInfo.tracking").value(nullValue()))
.andExpect(jsonPath("$.data.giftInfo.recipientCreatorNickname").value("erased-creator"))
.andExpect(jsonPath("$.data.trackingRequired").value(false))
.andExpect(jsonPath("$.data.recipientAddressRequired").value(false))
.andExpect(jsonPath("$.data.mailbox").value(nullValue()))
.andExpect(jsonPath("$.data.statusTimeline[4].occurredAt").value("2026-10-03T12:00:00Z"))
mockMvc.perform(get("/api/v2/gifts/{applicationNo}", gift.applicationNo).with(user(MemberAdapter(recipient))))
.andExpect(status().isOk)
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.direction").value("RECEIVED"))
.andExpect(jsonPath("$.data.senderInfo").value(nullValue()))
.andExpect(jsonPath("$.data.recipientAddress").value(nullValue()))
.andExpect(jsonPath("$.data.giftInfo.tracking").value(nullValue()))
.andExpect(jsonPath("$.data.giftInfo.senderNickname").value("erased-fan"))
.andExpect(jsonPath("$.data.trackingRequired").value(false))
.andExpect(jsonPath("$.data.recipientAddressRequired").value(false))
.andExpect(jsonPath("$.data.recipientAddressDeadlineAt").value("2026-10-07T12:00:00Z"))
}
@ParameterizedTest
@ValueSource(strings = ["senderName", "senderPhoneNumber", "senderZipCode", "senderAddress"])
@DisplayName("저장 필드가 nullable이어도 신청의 필수 발신자 입력 누락을 거부한다")
fun shouldKeepRequiredSenderRequestValidation(missingField: String) {
val member = Member(password = "password", nickname = "fan").apply { id = SENDER_ID }
val request = objectMapper.createObjectNode()
.put("recipientMemberId", RECIPIENT_ID)
.put("senderName", "sender")
.put("senderPhoneNumber", "phone")
.put("senderZipCode", "zip")
.put("senderAddress", "address")
.put("sizeCode", "SMALL")
.put("categoryId", CATEGORY_ID)
.put("senderTermsAgreed", true)
.put("senderPrivacyAgreed", true)
.put("damageWaiverAgreed", true)
request.remove(missingField)
mockMvc.perform(
post("/api/v2/gifts")
.with(user(MemberAdapter(member)))
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.success").value(false))
Mockito.verifyNoInteractions(commandService)
}
@Test
@DisplayName("발신자는 전달 완료 전 정상 진행 선물 상세에서 받을 주소를 조회한다")
fun shouldGetMailboxForSentNormalProgressGiftDetailBeforeDelivered() {
@@ -5,6 +5,7 @@ import kr.co.vividnext.sodalive.v2.gift.domain.GiftSize
import kr.co.vividnext.sodalive.v2.gift.domain.GiftStatus
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
@@ -13,6 +14,7 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.context.annotation.Import
import org.springframework.dao.DataIntegrityViolationException
import java.time.LocalDateTime
import javax.persistence.EntityManager
@DataJpaTest(
properties = [
@@ -28,7 +30,8 @@ class GiftEntityMappingTest @Autowired constructor(
private val giftCategoryRepository: GiftCategoryRepository,
private val giftSizePriceRepository: GiftSizePriceRepository,
private val giftReviewRepository: GiftReviewRepository,
private val giftApplicationNoSequenceRepository: GiftApplicationNoSequenceRepository
private val giftApplicationNoSequenceRepository: GiftApplicationNoSequenceRepository,
private val entityManager: EntityManager
) {
@Test
@DisplayName("선물 엔티티와 배송 정보를 저장하고 조회한다")
@@ -91,6 +94,63 @@ class GiftEntityMappingTest @Autowired constructor(
assertNotNull(saved.updatedAt)
}
@Test
@DisplayName("발신자와 수신자 및 운송장 개인정보를 모두 null로 저장하고 다시 조회한다")
fun shouldPersistNullForAllSenderFields() {
val category = giftCategoryRepository.save(giftCategory("100", "DOLL", "A"))
val gift = giftRepository.save(gift(category.id!!, "G-NULL-DELIVERY"))
val deadline = LocalDateTime.of(2026, 10, 2, 12, 0)
val delivery = giftDeliveryRepository.saveAndFlush(
GiftDelivery(
giftId = gift.id!!,
senderName = "sender",
senderPhoneNumber = "sender-phone",
senderZipCode = "sender-zip",
senderAddress = "sender-address",
senderAddressDetail = "sender-detail",
recipientName = "recipient",
recipientPhoneNumber = "recipient-phone",
recipientZipCode = "recipient-zip",
recipientAddress = "recipient-address",
recipientAddressDetail = "recipient-detail",
courierCompanyName = "courier",
trackingNumber = "tracking",
trackingDeadlineAt = deadline
)
)
delivery.senderName = null
delivery.senderPhoneNumber = null
delivery.senderZipCode = null
delivery.senderAddress = null
delivery.senderAddressDetail = null
delivery.recipientName = null
delivery.recipientPhoneNumber = null
delivery.recipientZipCode = null
delivery.recipientAddress = null
delivery.recipientAddressDetail = null
delivery.courierCompanyName = null
delivery.trackingNumber = null
entityManager.flush()
entityManager.clear()
val reloaded = requireNotNull(giftDeliveryRepository.findByGiftId(gift.id!!))
assertNull(reloaded.senderName)
assertNull(reloaded.senderPhoneNumber)
assertNull(reloaded.senderZipCode)
assertNull(reloaded.senderAddress)
assertNull(reloaded.senderAddressDetail)
assertNull(reloaded.recipientName)
assertNull(reloaded.recipientPhoneNumber)
assertNull(reloaded.recipientZipCode)
assertNull(reloaded.recipientAddress)
assertNull(reloaded.recipientAddressDetail)
assertNull(reloaded.courierCompanyName)
assertNull(reloaded.trackingNumber)
assertEquals(false, reloaded.hasCompleteRecipientInformation())
assertEquals(deadline, reloaded.trackingDeadlineAt)
}
@Test
@DisplayName("신청번호와 카테고리 분류번호 및 코드와 리뷰 선물 ID는 중복 저장할 수 없다")
fun shouldRejectDuplicateUniqueValues() {
@@ -13,13 +13,18 @@ import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftRepository
import kr.co.vividnext.sodalive.v2.gift.domain.GiftSize
import kr.co.vividnext.sodalive.v2.gift.domain.GiftStatus
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.NullAndEmptySource
import org.junit.jupiter.params.provider.ValueSource
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.context.annotation.Import
import java.time.LocalDateTime
import javax.persistence.EntityManager
@DataJpaTest(
properties = [
@@ -33,7 +38,8 @@ class GiftAdminQueryServiceTest @Autowired constructor(
private val giftRepository: GiftRepository,
private val deliveryRepository: GiftDeliveryRepository,
private val categoryRepository: GiftCategoryRepository,
private val memberRepository: MemberRepository
private val memberRepository: MemberRepository,
private val entityManager: EntityManager
) {
@Test
@DisplayName("관리자는 선물함 목록을 필터링하고 가능한 액션을 조회한다")
@@ -124,6 +130,51 @@ class GiftAdminQueryServiceTest @Autowired constructor(
assertEquals(listOf("COMPLETE_INSPECTION", "MARK_UNDELIVERABLE"), result.availableActions)
}
@Test
@DisplayName("개인정보가 지워진 관리자 선물 응답은 닉네임과 종료 상태 액션을 유지한다")
fun shouldMapErasedDeliveryStringsWithoutChangingActions() {
val sender = saveMember("fan-alpha")
val recipient = saveMember("creator-alpha")
val gift = saveGift("A-ERASED-DETAIL", sender, recipient, saveCategory(), GiftStatus.DELIVERED)
requireNotNull(deliveryRepository.findByGiftId(gift.id!!)).apply {
senderName = null
senderPhoneNumber = null
senderZipCode = null
senderAddress = null
senderAddressDetail = null
}
entityManager.flush()
entityManager.clear()
val detail = service().getGiftDetail(gift.applicationNo)
val item = service().getGifts(GiftStatus.DELIVERED, null, null, 0, 20).items.single()
assertEquals(AdminGiftMemberInfoResult("fan-alpha", "", "", ""), detail.senderInfo)
assertEquals(AdminGiftMemberInfoResult("creator-alpha", "", "", ""), detail.recipientInfo)
assertNull(detail.inboundDeliveryInfo.courierCompanyName)
assertNull(detail.inboundDeliveryInfo.trackingNumber)
assertEquals(GiftStatus.DELIVERED, detail.status)
assertEquals(emptyList<String>(), detail.availableActions)
assertEquals("fan-alpha", item.senderNickname)
assertEquals("creator-alpha", item.recipientNickname)
assertNull(item.courierCompanyName)
assertNull(item.trackingNumber)
assertEquals(emptyList<String>(), item.availableActions)
}
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = [" "])
@DisplayName("관리자 발신자 주소도 상세주소가 없으면 구분자를 생략한다")
fun shouldOmitAbsentSenderAddressDetail(detail: String?) {
val gift = saveGift("A-NO-DETAIL", saveMember("fan"), saveMember("creator"), saveCategory(), GiftStatus.RECEIVED)
requireNotNull(deliveryRepository.findByGiftId(gift.id!!)).senderAddressDetail = detail
val result = service().getGiftDetail(gift.applicationNo)
assertEquals("(06234) 서울시 강남구", result.senderInfo.address)
}
@Test
@DisplayName("관리자 선물 목록과 상세는 상태별 가능한 액션을 반환하고 없는 신청번호를 거부한다")
fun shouldReturnAvailableActionsByStatusAndRejectUnknownApplicationNo() {
@@ -27,10 +27,14 @@ import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.NullAndEmptySource
import org.junit.jupiter.params.provider.ValueSource
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.context.annotation.Import
import java.time.LocalDateTime
import javax.persistence.EntityManager
@DataJpaTest(
properties = [
@@ -46,7 +50,8 @@ class GiftQueryServiceTest @Autowired constructor(
private val deliveryRepository: GiftDeliveryRepository,
private val memberRepository: MemberRepository,
private val giftMailboxRepository: GiftMailboxRepository,
private val creatorLocalizedNameService: CreatorLocalizedNameService
private val creatorLocalizedNameService: CreatorLocalizedNameService,
private val entityManager: EntityManager
) {
@Test
fun `폼 옵션은 활성 카테고리와 활성 사이즈 가격만 반환한다`() {
@@ -313,6 +318,93 @@ class GiftQueryServiceTest @Autowired constructor(
assertNull(result.statusTimeline[1].occurredAt)
}
@Test
@DisplayName("개인정보가 지워진 보낸 선물은 발신자 객체와 빈 문자열을 유지한다")
fun shouldMapErasedSentSenderDetailsToEmptyStrings() {
val sender = saveMember("fan")
val recipient = saveMember("creator", "profile/creator.png")
val gift = saveGift("G-ERASED-SENT", sender.id!!, recipient.id!!, GiftStatus.DELIVERED)
saveDelivery(gift).apply {
applyStatusTimes(GiftStatus.DELIVERED)
recipientAddressDeadlineAt = NOW.plusDays(7)
senderName = null
senderPhoneNumber = null
senderZipCode = null
senderAddress = null
senderAddressDetail = null
}
entityManager.flush()
entityManager.clear()
val result = queryService().getGiftDetail(sender.id!!, gift.applicationNo, Lang.KO)
val item = queryService().getGifts(sender.id!!, GiftListType.SENT, 0, 20, Lang.KO).items.single()
assertEquals(GiftDirection.SENT, result.direction)
assertEquals(GiftStatus.DELIVERED, result.status)
assertEquals(GiftAddressResult("", "", ""), result.senderInfo)
assertNull(result.recipientAddress)
assertNull(result.giftInfo.tracking)
assertNull(result.mailbox)
assertEquals("creator", result.giftInfo.recipientCreatorNickname)
assertEquals(false, result.trackingRequired)
assertEquals(false, result.recipientAddressRequired)
assertEquals(NOW.plusHours(4), result.statusTimeline.last().occurredAt)
assertEquals("creator", item.counterpartNickname)
assertEquals("https://cdn.test/profile/creator.png", item.counterpartProfileImageUrl)
assertEquals(false, item.recipientAddressRegistered)
assertEquals(NOW.plusDays(7), item.recipientAddressDeadlineAt)
}
@Test
@DisplayName("개인정보가 지워진 받은 선물은 발신자 정보와 수신자 배송지를 null로 유지한다")
fun shouldKeepReceivedSenderInfoNullAndRecipientAddressNullWhenErased() {
val sender = saveMember("fan", "profile/fan.png")
val recipient = saveMember("creator")
val gift = saveGift("G-ERASED-RECEIVED", sender.id!!, recipient.id!!, GiftStatus.DELIVERED)
saveDelivery(gift).apply {
applyStatusTimes(GiftStatus.DELIVERED)
recipientAddressDeadlineAt = NOW.plusDays(7)
senderName = null
senderPhoneNumber = null
senderZipCode = null
senderAddress = null
senderAddressDetail = null
}
entityManager.flush()
entityManager.clear()
val result = queryService().getGiftDetail(recipient.id!!, gift.applicationNo, Lang.KO)
val item = queryService().getGifts(recipient.id!!, GiftListType.RECEIVED, 0, 20, Lang.KO).items.single()
assertEquals(GiftDirection.RECEIVED, result.direction)
assertNull(result.senderInfo)
assertNull(result.recipientAddress)
assertNull(result.giftInfo.tracking)
assertEquals("fan", result.giftInfo.senderNickname)
assertEquals(false, result.trackingRequired)
assertEquals(false, result.recipientAddressRequired)
assertEquals(NOW.plusDays(7), result.recipientAddressDeadlineAt)
assertEquals("fan", item.counterpartNickname)
assertEquals("https://cdn.test/profile/fan.png", item.counterpartProfileImageUrl)
assertEquals(false, item.recipientAddressRegistered)
assertEquals(NOW.plusDays(7), item.recipientAddressDeadlineAt)
}
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = [" "])
@DisplayName("발신자 상세주소가 없으면 주소 뒤 구분자를 생략한다")
fun shouldOmitAbsentSenderAddressDetail(detail: String?) {
val sender = saveMember("fan")
val recipient = saveMember("creator")
val gift = saveGift("G-NO-ADDRESS-DETAIL", sender.id!!, recipient.id!!, GiftStatus.RECEIVED)
saveDelivery(gift).senderAddressDetail = detail
val result = queryService().getGiftDetail(sender.id!!, gift.applicationNo, Lang.KO)
assertEquals("(06234) 서울시 강남구", result.senderInfo?.address)
}
@Test
@DisplayName("보낸 선물의 전달 완료 전 정상 진행 상세는 받을 주소를 반환한다")
fun shouldReturnMailboxForSentNormalProgressGiftDetailBeforeDelivered() {