From cd865da6e03e9ab904a23f8aff32e4ff992fdde3 Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 26 Oct 2026 10:26:12 +0900 Subject: [PATCH] =?UTF-8?q?fix(gift):=20=EB=88=84=EB=9D=BD=EB=90=9C=20?= =?UTF-8?q?=EB=B0=9C=EC=8B=A0=EC=9E=90=20=ED=91=B8=EC=8B=9C=EB=A5=BC=20?= =?UTF-8?q?=EB=B3=B4=EC=99=84=ED=95=98=EA=B3=A0=20=EA=B0=80=EA=B2=A9=20?= =?UTF-8?q?=EC=8A=A4=EB=83=85=EC=83=B7=EC=9D=84=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/out/scheduler/GiftScheduler.kt | 1 + .../v2/gift/application/GiftCommandService.kt | 1 + .../adapter/in/web/AdminGiftControllerTest.kt | 248 ++++++++++++++++++ .../v2/gift/GiftCommandServiceTest.kt | 22 +- .../out/scheduler/GiftSchedulerTest.kt | 9 + 5 files changed, 280 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftScheduler.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftScheduler.kt index 1c64c951..1f7e2960 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftScheduler.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftScheduler.kt @@ -85,6 +85,7 @@ class GiftScheduler( gift.status = GiftStatus.UNDELIVERABLE delivery.undeliverableAt = now delivery.undeliverableReason = EXPIRED_RECIPIENT_ADDRESS_REASON + giftPushService.sendUndeliverableToSender(gift.applicationNo, gift.senderMemberId) true } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftCommandService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftCommandService.kt index 47755809..88d49ebc 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftCommandService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftCommandService.kt @@ -301,6 +301,7 @@ class GiftCommandService( gift.status = GiftStatus.DELIVERED delivery.deliveredAt = deliveredAt + giftPushService.sendDeliveredToSender(gift.applicationNo, gift.senderMemberId) return GiftDeliveryConfirmationResult( applicationNo = gift.applicationNo, diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt index 13a343c0..2eac4430 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt @@ -33,9 +33,11 @@ import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftSizePrice import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftSizePriceRepository import kr.co.vividnext.sodalive.v2.gift.adapter.out.scheduler.GiftScheduler import kr.co.vividnext.sodalive.v2.gift.application.GiftAdminService +import kr.co.vividnext.sodalive.v2.gift.application.GiftApplicationCommand import kr.co.vividnext.sodalive.v2.gift.application.GiftCommandService import kr.co.vividnext.sodalive.v2.gift.application.GiftPushService import kr.co.vividnext.sodalive.v2.gift.application.GiftRecipientAddressRegistrationCommand +import kr.co.vividnext.sodalive.v2.gift.application.GiftSizePriceCommand import kr.co.vividnext.sodalive.v2.gift.application.GiftTrackingRegistrationCommand import kr.co.vividnext.sodalive.v2.gift.domain.GiftSize import kr.co.vividnext.sodalive.v2.gift.domain.GiftStatus @@ -167,6 +169,7 @@ class AdminGiftControllerTest @Autowired constructor( giftDeliveryRepository.deleteAll() giftRepository.deleteAll() categoryRepository.deleteAll() + sizePriceRepository.deleteAll() } @AfterEach @@ -544,6 +547,78 @@ class AdminGiftControllerTest @Autowired constructor( ) } + @Test + @DisplayName("실제 JWT HTTP 수령 확인은 발신자만 한 번 알리고 거부·반복 요청은 추가 알림을 보내지 않는다") + fun shouldConfirmDeliveryThroughRealJwtHttpAndNotifyOnlySenderOnce() { + val recipient = memberRepository.saveAndFlush( + Member( + email = "${UUID.randomUUID()}@test.com", + password = "password", + nickname = "confirmation-creator", + role = MemberRole.CREATOR + ) + ) + val fixture = saveFundedGift(recipient) + val giftId = requireNotNull(fixture.gift.id) + TransactionTemplate(transactionManager).executeWithoutResult { + giftRepository.findById(giftId).orElseThrow().status = GiftStatus.INSPECTION_COMPLETED + } + val path = "/api/v2/gifts/$APPLICATION_NO/delivery-complete" + giftHttp(HttpMethod.POST, path, expectedStatus = HttpStatus.UNAUTHORIZED) + val denied = giftHttp(HttpMethod.POST, path, tokenFor(fixture.sender)) + assertFalse(denied.path("success").asBoolean()) + assertEquals("Invalid request.", denied.path("message").asText()) + assertEquals(GiftStatus.INSPECTION_COMPLETED, giftRepository.findById(giftId).orElseThrow().status) + assertEquals(null, giftDeliveryRepository.findByGiftId(giftId)?.deliveredAt) + Mockito.verifyNoInteractions(giftPushService) + + val recipientToken = tokenFor(recipient) + val confirmed = giftHttp(HttpMethod.POST, path, recipientToken) + assertTrue(confirmed.path("success").asBoolean()) + assertEquals(APPLICATION_NO, confirmed.path("data").path("applicationNo").asText()) + assertEquals("DELIVERED", confirmed.path("data").path("status").asText()) + val deliveredAt = requireNotNull(giftDeliveryRepository.findByGiftId(giftId)?.deliveredAt) + assertEquals(deliveredAt.toUtcIso(), confirmed.path("data").path("deliveredAt").asText()) + assertEquals(GiftStatus.DELIVERED, giftRepository.findById(giftId).orElseThrow().status) + Mockito.verify(giftPushService).sendDeliveredToSender(APPLICATION_NO, requireNotNull(fixture.sender.id)) + Mockito.verifyNoMoreInteractions(giftPushService) + + val repeated = giftHttp(HttpMethod.POST, path, recipientToken) + assertFalse(repeated.path("success").asBoolean()) + assertEquals("Invalid request.", repeated.path("message").asText()) + assertEquals(deliveredAt, giftDeliveryRepository.findByGiftId(giftId)?.deliveredAt) + Mockito.verifyNoMoreInteractions(giftPushService) + assertRefundState(fixture, refunded = false) + println("P16_CONFIRM_HTTP status=200 success=true rejected=true repeated=false senderPush=1 recipientPush=0") + } + + @Test + @DisplayName("실제 transactional 스케줄러 배송지 만료는 발신자만 한 번 알리고 재실행은 추가 알림을 보내지 않는다") + fun shouldExpireRecipientAddressThroughTransactionalSchedulerAndNotifyOnlySenderOnce() { + val fixture = saveFundedGift() + val giftId = requireNotNull(fixture.gift.id) + TransactionTemplate(transactionManager).executeWithoutResult { + giftRepository.findById(giftId).orElseThrow().status = GiftStatus.TRACKING_REGISTERED + requireNotNull(giftDeliveryRepository.findByGiftId(giftId)).recipientAddressDeadlineAt = NOW.minusSeconds(1) + } + + assertFalse(TransactionSynchronizationManager.isActualTransactionActive()) + assertEquals(1, scheduler.markExpiredRecipientAddressGiftsUndeliverable(NOW)) + assertFalse(TransactionSynchronizationManager.isActualTransactionActive()) + assertEquals(GiftStatus.UNDELIVERABLE, giftRepository.findById(giftId).orElseThrow().status) + val delivery = requireNotNull(giftDeliveryRepository.findByGiftId(giftId)) + assertEquals(NOW, delivery.undeliverableAt) + assertEquals("배송지 미입력 기한 초과", delivery.undeliverableReason) + Mockito.verify(giftPushService).sendUndeliverableToSender(APPLICATION_NO, requireNotNull(fixture.sender.id)) + Mockito.verifyNoMoreInteractions(giftPushService) + + assertEquals(0, scheduler.markExpiredRecipientAddressGiftsUndeliverable(NOW.plusMinutes(1))) + assertEquals(NOW, giftDeliveryRepository.findByGiftId(giftId)?.undeliverableAt) + Mockito.verifyNoMoreInteractions(giftPushService) + assertRefundState(fixture, refunded = false) + println("P16_EXPIRY_DB applicationNo=$APPLICATION_NO transitioned=1 repeated=0 senderPush=1 recipientPush=0") + } + @Test @DisplayName("관리자는 카테고리를 등록, 조회, 수정, 논리삭제한다") fun shouldManageGiftCategories() { @@ -635,6 +710,166 @@ class AdminGiftControllerTest @Autowired constructor( .andExpect(jsonPath("$.success").value(false)) } + @Test + @DisplayName("실제 신청은 가격 변경과 카테고리 비활성화 후에도 기존 가격·카테고리명·결제 스냅샷을 보존한다") + fun shouldPreserveExistingGiftSnapshotsAfterPriceChangeAndCategoryDeactivation() { + val transaction = TransactionTemplate(transactionManager) + val sender = memberRepository.saveAndFlush( + Member(email = "${UUID.randomUUID()}@test.com", password = "password", nickname = "snapshot-sender").apply { + pgChargeCan = 300 + } + ) + val creator = memberRepository.saveAndFlush( + Member( + email = "${UUID.randomUUID()}@test.com", + password = "password", + nickname = "snapshot-creator", + role = MemberRole.CREATOR + ) + ) + assertEquals(creator.id, memberRepository.findCreatorByIdOrNull(requireNotNull(creator.id))?.id) + val funding = chargeRepository.saveAndFlush( + Charge(300, 0).apply { + member = sender + payment = Payment(status = PaymentStatus.COMPLETE, paymentGateway = PaymentGateway.PG) + } + ) + val category = categoryRepository.saveAndFlush( + GiftCategory("100", "SNAPSHOT", "인형", "A", "피규어") + ) + val categoryId = requireNotNull(category.id) + val senderId = requireNotNull(sender.id) + val originalUseCount = useCanRepository.count() + val originalCalculateCount = calculateRepository.count() + val originalChargeCount = chargeRepository.count() + adminService.updateSizePrice(GiftSize.SMALL, GiftSizePriceCommand(150, 80, true)) + val command = GiftApplicationCommand( + recipientMemberId = requireNotNull(creator.id), + senderName = "sender", + senderPhoneNumber = "01012345678", + senderZipCode = "06234", + senderAddress = "test address", + senderAddressDetail = null, + sizeCode = GiftSize.SMALL, + categoryId = categoryId, + senderTermsAgreed = true, + senderPrivacyAgreed = true, + damageWaiverAgreed = false + ) + + val first = commandService.registerGift(sender, command) + assertEquals(80, first.priceCan) + val firstUsageId = requireNotNull( + transaction.execute { + entityManager.flush() + entityManager.clear() + val gift = requireNotNull(giftRepository.findByApplicationNo(first.applicationNo)) + assertEquals(80, gift.salePriceCan) + assertEquals("인형", gift.categoryNameSnapshot) + assertEquals(80, useCanRepository.findById(gift.canUsageId).orElseThrow().can) + assertEquals(220, memberRepository.findById(senderId).orElseThrow().pgChargeCan) + assertEquals(220, chargeRepository.findById(requireNotNull(funding.id)).orElseThrow().chargeCan) + gift.canUsageId + } + ) + adminService.updateSizePrice(GiftSize.SMALL, GiftSizePriceCommand(150, 120, true)) + val second = commandService.registerGift(sender, command) + assertEquals(120, second.priceCan) + assertNotEquals(first.applicationNo, second.applicationNo) + val prices = mapOf(first.applicationNo to 80, second.applicationNo to 120) + val usageIds = prices.keys.associateWith { + requireNotNull(giftRepository.findByApplicationNo(it)).canUsageId + } + assertEquals(firstUsageId, usageIds[first.applicationNo]) + assertNotEquals(firstUsageId, usageIds[second.applicationNo]) + val senderToken = tokenFor(sender) + + listOf(false, true).forEach { deactivated -> + if (deactivated) { + assertFalse(adminService.deleteCategory(categoryId).isActive) + val exception = assertThrows(SodaException::class.java) { commandService.registerGift(sender, command) } + assertEquals("common.error.invalid_request", exception.messageKey) + } + transaction.executeWithoutResult { + entityManager.flush() + entityManager.clear() + assertEquals(!deactivated, categoryRepository.findById(categoryId).orElseThrow().isActive) + assertEquals(120, requireNotNull(sizePriceRepository.findBySizeCode(GiftSize.SMALL)).salePriceCan) + prices.forEach { (applicationNo, price) -> + val gift = requireNotNull(giftRepository.findByApplicationNo(applicationNo)) + assertEquals(price, gift.salePriceCan) + assertEquals("인형", gift.categoryNameSnapshot) + assertEquals(categoryId, gift.categoryId) + assertEquals(usageIds[applicationNo], gift.canUsageId) + val usage = useCanRepository.findById(gift.canUsageId).orElseThrow() + assertEquals(CanUsage.GIFT, usage.canUsage) + assertEquals(senderId, usage.member?.id) + assertEquals(price, usage.can) + assertEquals(0, usage.rewardCan) + assertFalse(usage.isRefund) + val calculations = calculateRepository.findByUseCanIdAndStatus( + gift.canUsageId, + UseCanCalculateStatus.RECEIVED + ) + assertEquals(1, calculations.size) + assertEquals(price, calculations.single().can) + assertEquals(PaymentGateway.PG, calculations.single().paymentGateway) + } + val reloadedSender = memberRepository.findById(senderId).orElseThrow() + val reloadedFunding = chargeRepository.findById(requireNotNull(funding.id)).orElseThrow() + assertEquals(100, reloadedSender.pgChargeCan) + assertEquals(0, reloadedSender.pgRewardCan) + assertEquals(100, reloadedFunding.chargeCan) + assertEquals(0, reloadedFunding.rewardCan) + assertEquals(ChargeStatus.CHARGE, reloadedFunding.status) + assertEquals(PaymentStatus.COMPLETE, reloadedFunding.payment?.status) + assertEquals(PaymentGateway.PG, reloadedFunding.payment?.paymentGateway) + assertEquals(2L, giftRepository.count()) + assertEquals(2L, giftDeliveryRepository.count()) + assertEquals(originalUseCount + 2, useCanRepository.count()) + assertEquals(originalCalculateCount + 2, calculateRepository.count()) + assertEquals(originalChargeCount, chargeRepository.count()) + } + val options = giftHttp(HttpMethod.GET, "/api/v2/gifts/form-options", senderToken) + assertTrue(options.path("success").asBoolean()) + assertEquals( + !deactivated, + options.path("data").path("categories").any { it.path("categoryId").asLong() == categoryId } + ) + assertEquals( + 120, + options.path("data").path("sizes").single { it.path("sizeCode").asText() == "SMALL" } + .path("salePriceCan").asInt() + ) + prices.forEach { (applicationNo, price) -> + val detail = giftHttp(HttpMethod.GET, "/api/v2/gifts/$applicationNo", senderToken) + assertTrue(detail.path("success").asBoolean()) + assertEquals(applicationNo, detail.path("data").path("applicationNo").asText()) + assertEquals(price, detail.path("data").path("giftInfo").path("paidCan").asInt()) + assertEquals("인형", detail.path("data").path("giftInfo").path("categoryName").asText()) + println("P16_SNAPSHOT_HTTP deactivated=$deactivated applicationNo=$applicationNo paidCan=$price categoryName=인형") + } + val list = giftHttp(HttpMethod.GET, "/api/v2/gifts?type=SENT", senderToken) + assertTrue(list.path("success").asBoolean()) + assertEquals(2, list.path("data").path("totalCount").asInt()) + val items = list.path("data").path("items").associateBy { it.path("applicationNo").asText() } + assertEquals(prices.keys, items.keys) + prices.forEach { (applicationNo, price) -> + val item = requireNotNull(items[applicationNo]) + assertEquals(price, item.path("priceCan").asInt()) + assertEquals("인형", item.path("categoryName").asText()) + } + println( + "P16_SNAPSHOT_DB deactivated=$deactivated A=80 B=120 categoryName=인형 usageIdsPreserved=true " + + "actualUseCan=80,120 memberBalance=100 chargeBalance=100 gifts=2 usesDelta=2 chargesDelta=0 listCount=2" + ) + } + Mockito.verify(canPaymentService).spendGiftCan(senderId, 80, "web") + Mockito.verify(canPaymentService).spendGiftCan(senderId, 120, "web") + assertEquals(2, Mockito.mockingDetails(canPaymentService).invocations.count { it.method.name == "spendGiftCan" }) + Mockito.verifyNoInteractions(giftPushService) + } + @Test @DisplayName("관리자는 사서함 도착과 검수완료를 처리한다") fun shouldUpdateGiftOperationStatuses() { @@ -1157,6 +1392,19 @@ class AdminGiftControllerTest @Autowired constructor( assertEquals(0, scheduler.markExpiredRecipientAddressGiftsUndeliverable(NOW)) Mockito.verify(giftPushService, Mockito.times(if (operations.first == RecipientMutation.REMINDER) 1 else 0)) .sendRecipientAddressDeadlineReminder(APPLICATION_NO, fixture.gift.recipientMemberId) + if ( + listOf(operations.first, operations.second).any { + it in listOf(RecipientMutation.CONFIRM, RecipientMutation.EXPIRY) + } + ) { + val confirmed = RecipientMutation.CONFIRM in listOf(operations.first, operations.second) && + operations.first != RecipientMutation.EXPIRY + Mockito.verify(giftPushService, Mockito.times(if (confirmed) 1 else 0)) + .sendDeliveredToSender(APPLICATION_NO, requireNotNull(fixture.sender.id)) + Mockito.verify(giftPushService, Mockito.times(if (operations.first == RecipientMutation.EXPIRY) 1 else 0)) + .sendUndeliverableToSender(APPLICATION_NO, requireNotNull(fixture.sender.id)) + Mockito.verifyNoMoreInteractions(giftPushService) + } assertRefundState(fixture, refunded = false) println( "P15_R1_RACE $operations first=$firstResult second=$secondResult serialized=$blockedUntilCommit persisted=true" diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/GiftCommandServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/GiftCommandServiceTest.kt index 7c94c356..5d7796d5 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/GiftCommandServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/GiftCommandServiceTest.kt @@ -241,7 +241,26 @@ class GiftCommandServiceTest { ) assertEquals(GiftStatus.DELIVERED, fixture.deliveryConfirmationGift.status) assertEquals(NOW, fixture.deliveryConfirmationDelivery.deliveredAt) - Mockito.verifyNoInteractions(fixture.giftPushService) + Mockito.verify(fixture.giftPushService).sendDeliveredToSender(APPLICATION_NO, SENDER_ID) + Mockito.verifyNoMoreInteractions(fixture.giftPushService) + } + + @Test + @DisplayName("수령 확인을 반복하면 전달 완료 시각을 보존하고 발신자에게 추가 알림을 보내지 않는다") + fun shouldNotNotifySenderAgainForRepeatedDeliveryConfirmation() { + val fixture = GiftApplicationFixture() + fixture.prepareGiftForDeliveryConfirmation() + fixture.service.confirmDelivery(fixture.creator, APPLICATION_NO) + + val exception = assertThrows(SodaException::class.java) { + fixture.service.confirmDelivery(fixture.creator, APPLICATION_NO) + } + + assertEquals("common.error.invalid_request", exception.messageKey) + assertEquals(GiftStatus.DELIVERED, fixture.deliveryConfirmationGift.status) + assertEquals(NOW, fixture.deliveryConfirmationDelivery.deliveredAt) + Mockito.verify(fixture.giftPushService).sendDeliveredToSender(APPLICATION_NO, SENDER_ID) + Mockito.verifyNoMoreInteractions(fixture.giftPushService) } @Test @@ -309,6 +328,7 @@ class GiftCommandServiceTest { assertEquals("common.error.invalid_request", exception.messageKey) assertEquals(scenario.status, fixture.deliveryConfirmationGift.status) assertEquals(null, fixture.deliveryConfirmationDelivery.deliveredAt) + Mockito.verifyNoInteractions(fixture.giftPushService) } @ParameterizedTest diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftSchedulerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftSchedulerTest.kt index 90b17aab..4278a082 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftSchedulerTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftSchedulerTest.kt @@ -415,6 +415,8 @@ class GiftSchedulerTest @Autowired constructor( assertEquals(GiftStatus.UNDELIVERABLE, gift.status) assertEquals(now, delivery.undeliverableAt) assertEquals("배송지 미입력 기한 초과", delivery.undeliverableReason) + Mockito.verify(giftPushService).sendUndeliverableToSender(APPLICATION_NO, SENDER_MEMBER_ID) + Mockito.verifyNoMoreInteractions(giftPushService) } @Test @@ -437,7 +439,9 @@ class GiftSchedulerTest @Autowired constructor( assertEquals(GiftStatus.UNDELIVERABLE, gift.status) assertEquals(now, delivery.undeliverableAt) assertEquals("배송지 미입력 기한 초과", delivery.undeliverableReason) + Mockito.verify(giftPushService).sendUndeliverableToSender(gift.applicationNo, SENDER_MEMBER_ID) } + Mockito.verifyNoMoreInteractions(giftPushService) } @Test @@ -456,6 +460,7 @@ class GiftSchedulerTest @Autowired constructor( assertEquals(GiftStatus.TRACKING_REGISTERED, gift.status) assertEquals(null, delivery.undeliverableAt) assertEquals(null, delivery.undeliverableReason) + Mockito.verifyNoInteractions(giftPushService) } @Test @@ -475,6 +480,7 @@ class GiftSchedulerTest @Autowired constructor( assertEquals(GiftStatus.TRACKING_REGISTERED, gift.status) assertEquals(null, delivery.undeliverableAt) assertEquals(null, delivery.undeliverableReason) + Mockito.verifyNoInteractions(giftPushService) } @Test @@ -515,6 +521,7 @@ class GiftSchedulerTest @Autowired constructor( assertEquals(null, delivery.undeliverableAt) assertEquals(null, delivery.undeliverableReason) } + Mockito.verifyNoInteractions(giftPushService) } @Test @@ -533,6 +540,8 @@ class GiftSchedulerTest @Autowired constructor( assertEquals(1, firstUpdatedCount) assertEquals(0, secondUpdatedCount) assertEquals(now, delivery.undeliverableAt) + Mockito.verify(giftPushService).sendUndeliverableToSender(APPLICATION_NO, SENDER_MEMBER_ID) + Mockito.verifyNoMoreInteractions(giftPushService) } @Test