From 0756c4d817c9163eb8c5ff82fe992c4c69b4e18f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=8D=ED=98=81=EC=A4=80?= <31675711+HyuckJuneHong@users.noreply.github.com> Date: Sun, 26 Nov 2023 13:12:55 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=BF=A0=ED=8F=B0=20=EB=B0=9C=EA=B8=89?= =?UTF-8?q?=20=EC=9A=94=EC=B2=AD=20=EB=B0=8F=20=EB=8C=80=EA=B8=B0=EC=97=B4?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=BF=A0=ED=8F=B0=20=EB=B0=9C?= =?UTF-8?q?=EA=B8=89=20=EC=B2=98=EB=A6=AC=20=EA=B5=AC=ED=98=84=20(#146)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * style : Schedule 어노테이션 위치 변경 * refactor: 쿠폰 발행 기간 하루로 통일 및 쿠폰 정보 오픈 날짜 추가 * feat: 쿠폰 발행 가능 날짜 중복 체크 기능 추가 * refactor: Builder 삭제 * test: 쿠폰 관련 테스트 수정 * feat: 쿠폰 발행 관련 레포지토리 기능 구현 및 테스트 * test: 쿠폰 발행 관련 문자열 레디스 기능 구현 및 테스트 * feat: 쿠폰 발행 관련 ZSET 레디스 기능 구현 및 테스트 * test: 쿠폰 발행 컨트롤러 기능 테스트 * test: RestDoc 업데이트 * test: Github Actions 시, Redis ZSET 명령어 못찾는 테스트 Disable --- .../api/application/bug/BugService.java | 2 +- .../coupon/CouponManageService.java | 91 ++++++++ .../coupon/CouponQueueService.java | 36 --- .../api/application/coupon/CouponService.java | 26 +-- .../com/moabam/api/domain/coupon/Coupon.java | 2 +- .../api/domain/coupon/CouponWallet.java | 6 +- .../repository/CouponManageRepository.java | 63 +++++ .../repository/CouponQueueRepository.java | 24 -- .../coupon/repository/CouponRepository.java | 7 +- .../redis/StringRedisRepository.java | 10 +- .../redis/ZSetRedisRepository.java | 21 +- .../api/presentation/CouponController.java | 8 +- .../global/common/util/DynamicQuery.java | 16 -- .../global/error/model/ErrorMessage.java | 1 + src/main/resources/config | 2 +- src/main/resources/static/docs/coupon.html | 6 +- .../resources/static/docs/notification.html | 2 +- .../api/application/bug/BugServiceTest.java | 2 +- .../coupon/CouponManageServiceTest.java | 215 ++++++++++++++++++ .../coupon/CouponQueueServiceTest.java | 83 ------- .../application/coupon/CouponServiceTest.java | 92 +++----- .../api/domain/coupon/CouponWalletTest.java | 5 +- .../CouponManageRepositoryTest.java | 172 ++++++++++++++ .../repository/CouponQueueRepositoryTest.java | 63 ----- .../CouponWalletSearchRepositoryTest.java | 2 +- .../redis/StringRedisRepositoryTest.java | 47 ++-- .../redis/ZSetRedisRepositoryTest.java | 69 ++++-- .../presentation/CouponControllerTest.java | 91 ++++++-- .../moabam/support/fixture/CouponFixture.java | 28 ++- 29 files changed, 797 insertions(+), 395 deletions(-) create mode 100644 src/main/java/com/moabam/api/application/coupon/CouponManageService.java delete mode 100644 src/main/java/com/moabam/api/application/coupon/CouponQueueService.java create mode 100644 src/main/java/com/moabam/api/domain/coupon/repository/CouponManageRepository.java delete mode 100644 src/main/java/com/moabam/api/domain/coupon/repository/CouponQueueRepository.java create mode 100644 src/test/java/com/moabam/api/application/coupon/CouponManageServiceTest.java delete mode 100644 src/test/java/com/moabam/api/application/coupon/CouponQueueServiceTest.java create mode 100644 src/test/java/com/moabam/api/domain/coupon/repository/CouponManageRepositoryTest.java delete mode 100644 src/test/java/com/moabam/api/domain/coupon/repository/CouponQueueRepositoryTest.java diff --git a/src/main/java/com/moabam/api/application/bug/BugService.java b/src/main/java/com/moabam/api/application/bug/BugService.java index de07033f..46908ae4 100644 --- a/src/main/java/com/moabam/api/application/bug/BugService.java +++ b/src/main/java/com/moabam/api/application/bug/BugService.java @@ -55,7 +55,7 @@ public PurchaseProductResponse purchaseBugProduct(Long memberId, Long productId, Payment payment = PaymentMapper.toPayment(memberId, product); if (!isNull(request.couponWalletId())) { - Coupon coupon = couponService.getByWallet(request.couponWalletId(), memberId); + Coupon coupon = couponService.getByWalletIdAndMemberId(request.couponWalletId(), memberId); payment.applyCoupon(coupon, request.couponWalletId()); } paymentRepository.save(payment); diff --git a/src/main/java/com/moabam/api/application/coupon/CouponManageService.java b/src/main/java/com/moabam/api/application/coupon/CouponManageService.java new file mode 100644 index 00000000..3fd13b1b --- /dev/null +++ b/src/main/java/com/moabam/api/application/coupon/CouponManageService.java @@ -0,0 +1,91 @@ +package com.moabam.api.application.coupon; + +import java.time.LocalDate; +import java.util.Optional; +import java.util.Set; + +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import com.moabam.api.domain.coupon.Coupon; +import com.moabam.api.domain.coupon.CouponWallet; +import com.moabam.api.domain.coupon.repository.CouponManageRepository; +import com.moabam.api.domain.coupon.repository.CouponRepository; +import com.moabam.api.domain.coupon.repository.CouponWalletRepository; +import com.moabam.global.auth.model.AuthMember; +import com.moabam.global.common.util.ClockHolder; +import com.moabam.global.error.exception.BadRequestException; +import com.moabam.global.error.model.ErrorMessage; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CouponManageService { + + private static final long ISSUE_SIZE = 10; + + private final ClockHolder clockHolder; + + private final CouponRepository couponRepository; + private final CouponManageRepository couponManageRepository; + private final CouponWalletRepository couponWalletRepository; + + @Scheduled(fixedDelay = 1000) + public void issue() { + LocalDate now = LocalDate.from(clockHolder.times()); + Optional isCoupon = couponRepository.findByStartAt(now); + + if (!canIssue(isCoupon)) { + return; + } + + Coupon coupon = isCoupon.get(); + Set membersId = couponManageRepository.popMinQueue(coupon.getName(), ISSUE_SIZE); + + membersId.forEach(memberId -> { + int nextStock = couponManageRepository.increaseIssuedStock(coupon.getName()); + + if (coupon.getStock() < nextStock) { + return; + } + + CouponWallet couponWallet = CouponWallet.create(memberId, coupon); + couponWalletRepository.save(couponWallet); + }); + } + + public void register(AuthMember authMember, String couponName) { + double registerTime = System.currentTimeMillis(); + validateRegister(couponName); + couponManageRepository.addIfAbsentQueue(couponName, authMember.id(), registerTime); + } + + public void deleteCouponManage(String couponName) { + couponManageRepository.deleteQueue(couponName); + couponManageRepository.deleteIssuedStock(couponName); + } + + private void validateRegister(String couponName) { + LocalDate now = LocalDate.from(clockHolder.times()); + Optional coupon = couponRepository.findByStartAt(now); + + if (coupon.isEmpty() || !coupon.get().getName().equals(couponName)) { + throw new BadRequestException(ErrorMessage.INVALID_COUPON_PERIOD); + } + } + + private boolean canIssue(Optional coupon) { + if (coupon.isEmpty()) { + return false; + } + + Coupon currentCoupon = coupon.get(); + int currentStock = couponManageRepository.getIssuedStock(currentCoupon.getName()); + int maxStock = currentCoupon.getStock(); + + return currentStock < maxStock; + } +} diff --git a/src/main/java/com/moabam/api/application/coupon/CouponQueueService.java b/src/main/java/com/moabam/api/application/coupon/CouponQueueService.java deleted file mode 100644 index 2d0c62e9..00000000 --- a/src/main/java/com/moabam/api/application/coupon/CouponQueueService.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.moabam.api.application.coupon; - -import org.springframework.stereotype.Service; - -import com.moabam.api.domain.coupon.Coupon; -import com.moabam.api.domain.coupon.repository.CouponQueueRepository; -import com.moabam.global.auth.model.AuthMember; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -@Service -@RequiredArgsConstructor -public class CouponQueueService { - - private final CouponService couponService; - private final CouponQueueRepository couponQueueRepository; - - public void register(AuthMember authMember, String couponName) { - double registerTime = System.currentTimeMillis(); - - if (canRegister(couponName)) { - log.info("{} 쿠폰이 모두 발급되었습니다.", couponName); - return; - } - - couponQueueRepository.addIfAbsent(couponName, authMember.nickname(), registerTime); - } - - private boolean canRegister(String couponName) { - Coupon coupon = couponService.validatePeriod(couponName); - - return coupon.getStock() <= couponQueueRepository.size(coupon.getName()); - } -} diff --git a/src/main/java/com/moabam/api/application/coupon/CouponService.java b/src/main/java/com/moabam/api/application/coupon/CouponService.java index cdc6fa44..b8310e7d 100644 --- a/src/main/java/com/moabam/api/application/coupon/CouponService.java +++ b/src/main/java/com/moabam/api/application/coupon/CouponService.java @@ -28,15 +28,18 @@ @Transactional(readOnly = true) public class CouponService { + private final ClockHolder clockHolder; + private final CouponManageService couponManageService; + private final CouponRepository couponRepository; private final CouponSearchRepository couponSearchRepository; private final CouponWalletSearchRepository couponWalletSearchRepository; - private final ClockHolder clockHolder; @Transactional public void create(AuthMember admin, CreateCouponRequest request) { validateAdminRole(admin); validateConflictName(request.name()); + validateConflictStartAt(request.startAt()); validatePeriod(request.startAt(), request.openAt()); Coupon coupon = CouponMapper.toEntity(admin.id(), request); @@ -49,6 +52,7 @@ public void delete(AuthMember admin, Long couponId) { Coupon coupon = couponRepository.findById(couponId) .orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_COUPON)); couponRepository.delete(coupon); + couponManageService.deleteCouponManage(coupon.getName()); } public CouponResponse getById(Long couponId) { @@ -58,7 +62,7 @@ public CouponResponse getById(Long couponId) { return CouponMapper.toDto(coupon); } - public Coupon getByWallet(Long couponWalletId, Long memberId) { + public Coupon getByWalletIdAndMemberId(Long couponWalletId, Long memberId) { return couponWalletSearchRepository.findByIdAndMemberId(couponWalletId, memberId) .orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_COUPON_WALLET)) .getCoupon(); @@ -73,18 +77,6 @@ public List getAllByStatus(CouponStatusRequest request) { .toList(); } - public Coupon validatePeriod(String couponName) { - LocalDate now = LocalDate.from(clockHolder.times()); - Coupon coupon = couponRepository.findByName(couponName) - .orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_COUPON)); - - if (!now.equals(coupon.getStartAt())) { - throw new BadRequestException(ErrorMessage.INVALID_COUPON_PERIOD); - } - - return coupon; - } - private void validatePeriod(LocalDate startAt, LocalDate openAt) { LocalDate now = LocalDate.from(clockHolder.times()); @@ -108,4 +100,10 @@ private void validateConflictName(String couponName) { throw new ConflictException(ErrorMessage.CONFLICT_COUPON_NAME); } } + + private void validateConflictStartAt(LocalDate startAt) { + if (couponRepository.existsByStartAt(startAt)) { + throw new ConflictException(ErrorMessage.CONFLICT_COUPON_START_AT); + } + } } diff --git a/src/main/java/com/moabam/api/domain/coupon/Coupon.java b/src/main/java/com/moabam/api/domain/coupon/Coupon.java index daeda469..e539d1b0 100644 --- a/src/main/java/com/moabam/api/domain/coupon/Coupon.java +++ b/src/main/java/com/moabam/api/domain/coupon/Coupon.java @@ -55,7 +55,7 @@ public class Coupon extends BaseTimeEntity { @Column(name = "stock", nullable = false) private int stock; - @Column(name = "start_at", nullable = false) + @Column(name = "start_at", unique = true, nullable = false) private LocalDate startAt; @Column(name = "open_at", nullable = false) diff --git a/src/main/java/com/moabam/api/domain/coupon/CouponWallet.java b/src/main/java/com/moabam/api/domain/coupon/CouponWallet.java index da0ff343..80994081 100644 --- a/src/main/java/com/moabam/api/domain/coupon/CouponWallet.java +++ b/src/main/java/com/moabam/api/domain/coupon/CouponWallet.java @@ -12,7 +12,6 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import lombok.AccessLevel; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -34,9 +33,12 @@ public class CouponWallet extends BaseTimeEntity { @ManyToOne(fetch = FetchType.LAZY) private Coupon coupon; - @Builder private CouponWallet(Long memberId, Coupon coupon) { this.memberId = memberId; this.coupon = coupon; } + + public static CouponWallet create(Long memberId, Coupon coupon) { + return new CouponWallet(memberId, coupon); + } } diff --git a/src/main/java/com/moabam/api/domain/coupon/repository/CouponManageRepository.java b/src/main/java/com/moabam/api/domain/coupon/repository/CouponManageRepository.java new file mode 100644 index 00000000..b2aa3814 --- /dev/null +++ b/src/main/java/com/moabam/api/domain/coupon/repository/CouponManageRepository.java @@ -0,0 +1,63 @@ +package com.moabam.api.domain.coupon.repository; + +import static java.util.Objects.*; + +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Repository; + +import com.moabam.api.infrastructure.redis.StringRedisRepository; +import com.moabam.api.infrastructure.redis.ZSetRedisRepository; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class CouponManageRepository { + + private static final String STOCK_KEY = "%s_INCR"; + + private final ZSetRedisRepository zSetRedisRepository; + private final StringRedisRepository stringRedisRepository; + + public void addIfAbsentQueue(String couponName, Long memberId, double registerTime) { + zSetRedisRepository.addIfAbsent(requireNonNull(couponName), requireNonNull(memberId), registerTime); + } + + public Set popMinQueue(String couponName, long count) { + return zSetRedisRepository + .popMin(requireNonNull(couponName), count) + .stream() + .map(tuple -> (Long)tuple.getValue()) + .collect(Collectors.toSet()); + } + + public void deleteQueue(String couponName) { + stringRedisRepository.delete(requireNonNull(couponName)); + } + + public int increaseIssuedStock(String couponName) { + String stockKey = String.format(STOCK_KEY, requireNonNull(couponName)); + + return stringRedisRepository + .increment(requireNonNull(stockKey)) + .intValue(); + } + + public int getIssuedStock(String couponName) { + String stockKey = String.format(STOCK_KEY, requireNonNull(couponName)); + String stockValue = stringRedisRepository.get(requireNonNull(stockKey)); + + if (stockValue == null) { + return 0; + } + + return Integer.parseInt(stockValue); + } + + public void deleteIssuedStock(String couponName) { + String stockKey = String.format(STOCK_KEY, requireNonNull(couponName)); + stringRedisRepository.delete(requireNonNull(stockKey)); + } +} diff --git a/src/main/java/com/moabam/api/domain/coupon/repository/CouponQueueRepository.java b/src/main/java/com/moabam/api/domain/coupon/repository/CouponQueueRepository.java deleted file mode 100644 index a7ab35ef..00000000 --- a/src/main/java/com/moabam/api/domain/coupon/repository/CouponQueueRepository.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.moabam.api.domain.coupon.repository; - -import static java.util.Objects.*; - -import org.springframework.stereotype.Repository; - -import com.moabam.api.infrastructure.redis.ZSetRedisRepository; - -import lombok.RequiredArgsConstructor; - -@Repository -@RequiredArgsConstructor -public class CouponQueueRepository { - - private final ZSetRedisRepository zSetRedisRepository; - - public void addIfAbsent(String couponName, String memberNickname, double score) { - zSetRedisRepository.addIfAbsent(requireNonNull(couponName), requireNonNull(memberNickname), score); - } - - public Long size(String couponName) { - return zSetRedisRepository.size(requireNonNull(couponName)); - } -} diff --git a/src/main/java/com/moabam/api/domain/coupon/repository/CouponRepository.java b/src/main/java/com/moabam/api/domain/coupon/repository/CouponRepository.java index 02760025..38b9dbe1 100644 --- a/src/main/java/com/moabam/api/domain/coupon/repository/CouponRepository.java +++ b/src/main/java/com/moabam/api/domain/coupon/repository/CouponRepository.java @@ -1,5 +1,6 @@ package com.moabam.api.domain.coupon.repository; +import java.time.LocalDate; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; @@ -8,7 +9,9 @@ public interface CouponRepository extends JpaRepository { - Optional findByName(String couponName); - boolean existsByName(String name); + + boolean existsByStartAt(LocalDate startAt); + + Optional findByStartAt(LocalDate startAt); } diff --git a/src/main/java/com/moabam/api/infrastructure/redis/StringRedisRepository.java b/src/main/java/com/moabam/api/infrastructure/redis/StringRedisRepository.java index ce1f067b..c5d4d9df 100644 --- a/src/main/java/com/moabam/api/infrastructure/redis/StringRedisRepository.java +++ b/src/main/java/com/moabam/api/infrastructure/redis/StringRedisRepository.java @@ -19,8 +19,10 @@ public void save(String key, String value, Duration timeout) { .set(key, value, timeout); } - public void delete(String key) { - redisTemplate.delete(key); + public Long increment(String key) { + return redisTemplate + .opsForValue() + .increment(key); } public String get(String key) { @@ -32,4 +34,8 @@ public String get(String key) { public Boolean hasKey(String key) { return redisTemplate.hasKey(key); } + + public void delete(String key) { + redisTemplate.delete(key); + } } diff --git a/src/main/java/com/moabam/api/infrastructure/redis/ZSetRedisRepository.java b/src/main/java/com/moabam/api/infrastructure/redis/ZSetRedisRepository.java index e02a3c4e..9ac7c607 100644 --- a/src/main/java/com/moabam/api/infrastructure/redis/ZSetRedisRepository.java +++ b/src/main/java/com/moabam/api/infrastructure/redis/ZSetRedisRepository.java @@ -1,6 +1,11 @@ package com.moabam.api.infrastructure.redis; +import static java.util.Objects.*; + +import java.util.Set; + import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ZSetOperations.TypedTuple; import org.springframework.stereotype.Repository; import lombok.RequiredArgsConstructor; @@ -11,25 +16,17 @@ public class ZSetRedisRepository { private final RedisTemplate redisTemplate; - public void addIfAbsent(String key, String value, double score) { + public void addIfAbsent(String key, Object value, double score) { if (redisTemplate.opsForZSet().score(key, value) == null) { redisTemplate .opsForZSet() - .add(key, value, score); + .add(requireNonNull(key), requireNonNull(value), score); } } - public Long size(String key) { + public Set> popMin(String key, long count) { return redisTemplate .opsForZSet() - .size(key); - } - - public Boolean hasKey(String key) { - return redisTemplate.hasKey(key); - } - - public void delete(String key) { - redisTemplate.delete(key); + .popMin(key, count); } } diff --git a/src/main/java/com/moabam/api/presentation/CouponController.java b/src/main/java/com/moabam/api/presentation/CouponController.java index 41d817ae..e5b03746 100644 --- a/src/main/java/com/moabam/api/presentation/CouponController.java +++ b/src/main/java/com/moabam/api/presentation/CouponController.java @@ -12,7 +12,7 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; -import com.moabam.api.application.coupon.CouponQueueService; +import com.moabam.api.application.coupon.CouponManageService; import com.moabam.api.application.coupon.CouponService; import com.moabam.api.dto.coupon.CouponResponse; import com.moabam.api.dto.coupon.CouponStatusRequest; @@ -28,7 +28,7 @@ public class CouponController { private final CouponService couponService; - private final CouponQueueService couponQueueService; + private final CouponManageService couponManageService; @PostMapping("/admins/coupons") @ResponseStatus(HttpStatus.CREATED) @@ -55,7 +55,7 @@ public List getAllByStatus(@Valid @RequestBody CouponStatusReque } @PostMapping("/coupons") - public void registerCouponQueue(@Auth AuthMember authMember, @RequestParam("couponName") String couponName) { - couponQueueService.register(authMember, couponName); + public void registerQueue(@Auth AuthMember authMember, @RequestParam("couponName") String couponName) { + couponManageService.register(authMember, couponName); } } diff --git a/src/main/java/com/moabam/global/common/util/DynamicQuery.java b/src/main/java/com/moabam/global/common/util/DynamicQuery.java index c3ea6069..47468ee9 100644 --- a/src/main/java/com/moabam/global/common/util/DynamicQuery.java +++ b/src/main/java/com/moabam/global/common/util/DynamicQuery.java @@ -1,12 +1,8 @@ package com.moabam.global.common.util; -import java.util.List; import java.util.Objects; -import java.util.Optional; import java.util.function.Function; -import org.springframework.util.CollectionUtils; - import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.SimpleExpression; @@ -35,16 +31,4 @@ public static BooleanExpression generateIsNull(Bool return field.isNotNull(); } - - public static BooleanExpression filterCondition(T condition, Function function) { - T tempCondition = condition; - - if (tempCondition instanceof List c && CollectionUtils.isEmpty(c)) { - tempCondition = null; - } - - return Optional.ofNullable(tempCondition) - .map(function) - .orElse(null); - } } diff --git a/src/main/java/com/moabam/global/error/model/ErrorMessage.java b/src/main/java/com/moabam/global/error/model/ErrorMessage.java index ddc96335..0fc50530 100644 --- a/src/main/java/com/moabam/global/error/model/ErrorMessage.java +++ b/src/main/java/com/moabam/global/error/model/ErrorMessage.java @@ -70,6 +70,7 @@ public enum ErrorMessage { INVALID_COUPON_OPEN_AT_PERIOD("쿠폰 정보 오픈 날짜는 시작 날짜보다 이전이여야 합니다."), INVALID_COUPON_PERIOD("쿠폰 발급 가능 기간이 아닙니다."), CONFLICT_COUPON_NAME("쿠폰의 이름이 중복되었습니다."), + CONFLICT_COUPON_START_AT("쿠폰 발급 가능 날짜가 중복되었습니다."), NOT_FOUND_COUPON_TYPE("존재하지 않는 쿠폰 종류입니다."), NOT_FOUND_COUPON("존재하지 않는 쿠폰입니다."), NOT_FOUND_COUPON_WALLET("보유하지 않은 쿠폰입니다."), diff --git a/src/main/resources/config b/src/main/resources/config index 2e460460..2a1a59a1 160000 --- a/src/main/resources/config +++ b/src/main/resources/config @@ -1 +1 @@ -Subproject commit 2e460460a0048796a3ef6fef17697935027a8708 +Subproject commit 2a1a59a16d8e868185c125a58aec0682f3c53f0d diff --git a/src/main/resources/static/docs/coupon.html b/src/main/resources/static/docs/coupon.html index d31fe33a..5678c54e 100644 --- a/src/main/resources/static/docs/coupon.html +++ b/src/main/resources/static/docs/coupon.html @@ -537,7 +537,7 @@

응답

Content-Length: 215 { - "id" : 26, + "id" : 16, "adminName" : "1admin", "name" : "couponName", "description" : "", @@ -585,7 +585,7 @@

응답

Content-Length: 216 [ { - "id" : 15, + "id" : 17, "adminName" : "1admin", "name" : "coupon1", "description" : "", @@ -627,7 +627,7 @@

응답

Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers Content-Type: application/json -Content-Length: 66 +Content-Length: 64 { "message" : "쿠폰 발급 가능 기간이 아닙니다." diff --git a/src/main/resources/static/docs/notification.html b/src/main/resources/static/docs/notification.html index 38ed7f63..3340a231 100644 --- a/src/main/resources/static/docs/notification.html +++ b/src/main/resources/static/docs/notification.html @@ -473,7 +473,7 @@

응답

Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers Content-Type: application/json -Content-Length: 66 +Content-Length: 64 { "message" : "해당 유저는 접속 중이 아닙니다." diff --git a/src/test/java/com/moabam/api/application/bug/BugServiceTest.java b/src/test/java/com/moabam/api/application/bug/BugServiceTest.java index 0d4bce1f..1cde9be1 100644 --- a/src/test/java/com/moabam/api/application/bug/BugServiceTest.java +++ b/src/test/java/com/moabam/api/application/bug/BugServiceTest.java @@ -103,7 +103,7 @@ void apply_coupon_success() { PurchaseProductRequest request = new PurchaseProductRequest(couponWalletId); given(productRepository.findById(productId)).willReturn(Optional.of(bugProduct())); given(paymentRepository.save(any(Payment.class))).willReturn(payment); - given(couponService.getByWallet(couponWalletId, memberId)).willReturn(discount1000Coupon()); + given(couponService.getByWalletIdAndMemberId(couponWalletId, memberId)).willReturn(discount1000Coupon()); // when PurchaseProductResponse response = bugService.purchaseBugProduct(memberId, productId, request); diff --git a/src/test/java/com/moabam/api/application/coupon/CouponManageServiceTest.java b/src/test/java/com/moabam/api/application/coupon/CouponManageServiceTest.java new file mode 100644 index 00000000..214ca0ab --- /dev/null +++ b/src/test/java/com/moabam/api/application/coupon/CouponManageServiceTest.java @@ -0,0 +1,215 @@ +package com.moabam.api.application.coupon; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.*; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.moabam.api.domain.coupon.Coupon; +import com.moabam.api.domain.coupon.CouponWallet; +import com.moabam.api.domain.coupon.repository.CouponManageRepository; +import com.moabam.api.domain.coupon.repository.CouponRepository; +import com.moabam.api.domain.coupon.repository.CouponWalletRepository; +import com.moabam.global.auth.model.AuthMember; +import com.moabam.global.auth.model.AuthorizationThreadLocal; +import com.moabam.global.common.util.ClockHolder; +import com.moabam.global.error.exception.BadRequestException; +import com.moabam.global.error.model.ErrorMessage; +import com.moabam.support.annotation.WithMember; +import com.moabam.support.common.FilterProcessExtension; +import com.moabam.support.fixture.CouponFixture; + +@ExtendWith({MockitoExtension.class, FilterProcessExtension.class}) +class CouponManageServiceTest { + + @InjectMocks + CouponManageService couponManageService; + + @Mock + CouponRepository couponRepository; + + @Mock + CouponManageRepository couponManageRepository; + + @Mock + CouponWalletRepository couponWalletRepository; + + @Mock + ClockHolder clockHolder; + + @DisplayName("쿠폰 발행이 성공적으로 된다.") + @Test + void issue_all_success() { + // Given + Coupon coupon = CouponFixture.coupon(1000, 100); + Set membersId = new HashSet<>(Set.of(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L)); + + given(clockHolder.times()).willReturn(LocalDateTime.now()); + given(couponRepository.findByStartAt(any(LocalDate.class))).willReturn(Optional.of(coupon)); + given(couponManageRepository.getIssuedStock(any(String.class))).willReturn(10); + given(couponManageRepository.popMinQueue(any(String.class), any(long.class))).willReturn(membersId); + given(couponManageRepository.increaseIssuedStock(any(String.class))).willReturn(99); + + // When + couponManageService.issue(); + + // Then + verify(couponWalletRepository, times(10)).save(any(CouponWallet.class)); + } + + @DisplayName("발행 가능한 쿠폰이 없다.") + @Test + void issue_notStartAt() { + // Given + given(clockHolder.times()).willReturn(LocalDateTime.now()); + given(couponRepository.findByStartAt(any(LocalDate.class))).willReturn(Optional.empty()); + + // When + couponManageService.issue(); + + // Then + verify(couponManageRepository, times(0)).getIssuedStock(any(String.class)); + verify(couponManageRepository, times(0)).popMinQueue(any(String.class), any(long.class)); + verify(couponManageRepository, times(0)).increaseIssuedStock(any(String.class)); + verify(couponWalletRepository, times(0)).save(any(CouponWallet.class)); + } + + @DisplayName("해당 쿠폰은 재고가 마감된 쿠폰이다.") + @Test + void issue_stockEnd() { + // Given + Coupon coupon = CouponFixture.coupon(1000, 100); + + given(clockHolder.times()).willReturn(LocalDateTime.now()); + given(couponRepository.findByStartAt(any(LocalDate.class))).willReturn(Optional.of(coupon)); + given(couponManageRepository.getIssuedStock(any(String.class))).willReturn(coupon.getStock()); + + // When + couponManageService.issue(); + + // Then + verify(couponManageRepository, times(0)).popMinQueue(any(String.class), any(long.class)); + verify(couponManageRepository, times(0)).increaseIssuedStock(any(String.class)); + verify(couponWalletRepository, times(0)).save(any(CouponWallet.class)); + } + + @DisplayName("대기열에 남은 인원이 모두 발급받지 못한다.") + @Test + void issue_queue_stockENd() { + // Given + Coupon coupon = CouponFixture.coupon(1000, 100); + Set membersId = new HashSet<>(Set.of(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L)); + + given(clockHolder.times()).willReturn(LocalDateTime.now()); + given(couponRepository.findByStartAt(any(LocalDate.class))).willReturn(Optional.of(coupon)); + given(couponManageRepository.getIssuedStock(any(String.class))).willReturn(10); + given(couponManageRepository.popMinQueue(any(String.class), any(long.class))).willReturn(membersId); + given(couponManageRepository.increaseIssuedStock(any(String.class))).willReturn(101); + + // When + couponManageService.issue(); + + // Then + verify(couponWalletRepository, times(0)).save(any(CouponWallet.class)); + } + + @WithMember + @DisplayName("쿠폰 발급 요청을 성공적으로 큐에 등록한다. - Void") + @Test + void register_success() { + // Given + AuthMember member = AuthorizationThreadLocal.getAuthMember(); + Coupon coupon = CouponFixture.coupon(); + + given(clockHolder.times()).willReturn(LocalDateTime.now()); + given(couponRepository.findByStartAt(any(LocalDate.class))).willReturn(Optional.of(coupon)); + + // When + couponManageService.register(member, coupon.getName()); + + // Then + verify(couponManageRepository).addIfAbsentQueue(any(String.class), any(Long.class), any(double.class)); + } + + @WithMember + @DisplayName("금일 발급이 가능한 쿠폰이 없다. - BadRequestException") + @Test + void register_StartAt_BadRequestException() { + // Given + AuthMember member = AuthorizationThreadLocal.getAuthMember(); + + given(clockHolder.times()).willReturn(LocalDateTime.now()); + given(couponRepository.findByStartAt(any(LocalDate.class))).willReturn(Optional.empty()); + + // When & Then + assertThatThrownBy(() -> couponManageService.register(member, "couponName")) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorMessage.INVALID_COUPON_PERIOD.getMessage()); + } + + @WithMember + @DisplayName("금일 발급 가능한 쿠폰의 이름과 일치하지 않는다. - BadRequestException") + @Test + void register_Name_BadRequestException() { + // Given + AuthMember member = AuthorizationThreadLocal.getAuthMember(); + Coupon coupon = CouponFixture.coupon(); + + given(clockHolder.times()).willReturn(LocalDateTime.now()); + given(couponRepository.findByStartAt(any(LocalDate.class))).willReturn(Optional.of(coupon)); + + // When & Then + assertThatThrownBy(() -> couponManageService.register(member, "Coupon Cannot Be Issued Today")) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorMessage.INVALID_COUPON_PERIOD.getMessage()); + } + + @DisplayName("쿠폰 대기열과 발행된 재고가 정상적으로 삭제된다.") + @Test + void deleteCouponManage_success() { + // Given + String couponName = "couponName"; + + // When + couponManageService.deleteCouponManage(couponName); + + // Then + verify(couponManageRepository).deleteQueue(couponName); + verify(couponManageRepository).deleteIssuedStock(couponName); + } + + @DisplayName("쿠폰 대기열이 정상적으로 삭제되지 않는다.") + @Test + void deleteCouponManage_Queue_NullPointerException() { + // Given + willThrow(NullPointerException.class).given(couponManageRepository).deleteQueue(any(String.class)); + + // When & Then + assertThatThrownBy(() -> couponManageService.deleteCouponManage("null")) + .isInstanceOf(NullPointerException.class); + } + + @DisplayName("쿠폰의 발행된 재고가 정상적으로 삭제되지 않는다.") + @Test + void deleteCouponManage_Stock_NullPointerException() { + // Given + willDoNothing().given(couponManageRepository).deleteQueue(any(String.class)); + willThrow(NullPointerException.class).given(couponManageRepository).deleteIssuedStock(any(String.class)); + + // When & Then + assertThatThrownBy(() -> couponManageService.deleteCouponManage("null")) + .isInstanceOf(NullPointerException.class); + } +} diff --git a/src/test/java/com/moabam/api/application/coupon/CouponQueueServiceTest.java b/src/test/java/com/moabam/api/application/coupon/CouponQueueServiceTest.java deleted file mode 100644 index b6721d68..00000000 --- a/src/test/java/com/moabam/api/application/coupon/CouponQueueServiceTest.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.moabam.api.application.coupon; - -import static org.assertj.core.api.Assertions.*; -import static org.mockito.BDDMockito.*; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import com.moabam.api.domain.coupon.Coupon; -import com.moabam.api.domain.coupon.repository.CouponQueueRepository; -import com.moabam.global.auth.model.AuthMember; -import com.moabam.global.auth.model.AuthorizationThreadLocal; -import com.moabam.global.error.exception.BadRequestException; -import com.moabam.global.error.model.ErrorMessage; -import com.moabam.support.annotation.WithMember; -import com.moabam.support.common.FilterProcessExtension; -import com.moabam.support.fixture.CouponFixture; - -@ExtendWith({MockitoExtension.class, FilterProcessExtension.class}) -class CouponQueueServiceTest { - - @InjectMocks - private CouponQueueService couponQueueService; - - @Mock - private CouponQueueRepository couponQueueRepository; - - @Mock - private CouponService couponService; - - @WithMember - @DisplayName("쿠폰 발급 요청을 성공적으로 큐에 등록한다. - Void") - @Test - void register() { - // Given - AuthMember member = AuthorizationThreadLocal.getAuthMember(); - Coupon coupon = CouponFixture.coupon(); - - given(couponService.validatePeriod(any(String.class))).willReturn(coupon); - given(couponQueueRepository.size(any(String.class))).willReturn(coupon.getStock() - 1L); - - // When - couponQueueService.register(member, coupon.getName()); - - // Then - verify(couponQueueRepository).addIfAbsent(any(String.class), any(String.class), any(double.class)); - } - - @WithMember - @DisplayName("해당 쿠폰은 발급 가능 기간이 아니다. - BadRequestException") - @Test - void register_BadRequestException() { - // Given - AuthMember member = AuthorizationThreadLocal.getAuthMember(); - - given(couponService.validatePeriod(any(String.class))) - .willThrow(new BadRequestException(ErrorMessage.INVALID_COUPON_PERIOD)); - - // When & Then - assertThatThrownBy(() -> couponQueueService.register(member, "couponName")) - .isInstanceOf(BadRequestException.class) - .hasMessage(ErrorMessage.INVALID_COUPON_PERIOD.getMessage()); - } - - @WithMember - @DisplayName("해당 쿠폰은 마감된 쿠폰이다. - Void") - @Test - void register_End() { - // Given - AuthMember member = AuthorizationThreadLocal.getAuthMember(); - Coupon coupon = CouponFixture.coupon(); - - given(couponService.validatePeriod(any(String.class))).willReturn(coupon); - given(couponQueueRepository.size(any(String.class))).willReturn((long)coupon.getStock()); - - // When & Then - assertThatNoException().isThrownBy(() -> couponQueueService.register(member, coupon.getName())); - } -} diff --git a/src/test/java/com/moabam/api/application/coupon/CouponServiceTest.java b/src/test/java/com/moabam/api/application/coupon/CouponServiceTest.java index 18099a8c..d929f46b 100644 --- a/src/test/java/com/moabam/api/application/coupon/CouponServiceTest.java +++ b/src/test/java/com/moabam/api/application/coupon/CouponServiceTest.java @@ -40,21 +40,24 @@ class CouponServiceTest { @InjectMocks - private CouponService couponService; + CouponService couponService; @Mock - private CouponRepository couponRepository; + CouponManageService couponManageService; @Mock - private CouponSearchRepository couponSearchRepository; + CouponRepository couponRepository; @Mock - private ClockHolder clockHolder; + CouponSearchRepository couponSearchRepository; + + @Mock + ClockHolder clockHolder; @WithMember(role = Role.ADMIN) @DisplayName("쿠폰을 성공적으로 발행한다. - Void") @Test - void create() { + void create_success() { // Given AuthMember admin = AuthorizationThreadLocal.getAuthMember(); CreateCouponRequest request = CouponFixture.createCouponRequest(); @@ -103,7 +106,7 @@ void create_Type_NotFoundException() { @WithMember(role = Role.ADMIN) @DisplayName("중복된 쿠폰명을 발행한다. - ConflictException") @Test - void create_ConflictException() { + void create_Name_ConflictException() { // Given AuthMember admin = AuthorizationThreadLocal.getAuthMember(); CreateCouponRequest request = CouponFixture.createCouponRequest(); @@ -116,6 +119,23 @@ void create_ConflictException() { .hasMessage(ErrorMessage.CONFLICT_COUPON_NAME.getMessage()); } + @WithMember(role = Role.ADMIN) + @DisplayName("중복된 쿠폰 발행 가능 날짜를 발행한다. - ConflictException") + @Test + void create_StartAt_ConflictException() { + // Given + AuthMember admin = AuthorizationThreadLocal.getAuthMember(); + CreateCouponRequest request = CouponFixture.createCouponRequest(); + + given(couponRepository.existsByName(any(String.class))).willReturn(false); + given(couponRepository.existsByStartAt(any(LocalDate.class))).willReturn(true); + + // When & Then + assertThatThrownBy(() -> couponService.create(admin, request)) + .isInstanceOf(ConflictException.class) + .hasMessage(ErrorMessage.CONFLICT_COUPON_START_AT.getMessage()); + } + @WithMember(role = Role.ADMIN) @DisplayName("현재 날짜가 쿠폰 발급 가능 날짜와 같거나 이후이다. - BadRequestException") @Test @@ -126,6 +146,7 @@ void create_StartAt_BadRequestException() { given(clockHolder.times()).willReturn(LocalDateTime.of(2025, 1, 1, 1, 1)); given(couponRepository.existsByName(any(String.class))).willReturn(false); + given(couponRepository.existsByStartAt(any(LocalDate.class))).willReturn(false); // When & Then assertThatThrownBy(() -> couponService.create(admin, request)) @@ -143,6 +164,7 @@ void create_OpenAt_BadRequestException() { CreateCouponRequest request = CouponFixture.createCouponRequest(couponType, 1, 1); given(couponRepository.existsByName(any(String.class))).willReturn(false); + given(couponRepository.existsByStartAt(any(LocalDate.class))).willReturn(false); given(clockHolder.times()).willReturn(LocalDateTime.of(2022, 1, 1, 1, 1)); // When & Then @@ -152,9 +174,9 @@ void create_OpenAt_BadRequestException() { } @WithMember(role = Role.ADMIN) - @DisplayName("쿠폰 아이디와 일치하는 쿠폰을 삭제한다. - Void") + @DisplayName("쿠폰 아이디와 일치하는 쿠폰을 성공적으로 삭제한다. - Void") @Test - void delete() { + void delete_success() { // Given AuthMember admin = AuthorizationThreadLocal.getAuthMember(); Coupon coupon = CouponFixture.coupon(10, 100); @@ -165,6 +187,7 @@ void delete() { // Then verify(couponRepository).delete(coupon); + verify(couponManageService).deleteCouponManage(any(String.class)); } @WithMember(role = Role.USER) @@ -194,9 +217,9 @@ void delete_NotFoundException() { .hasMessage(ErrorMessage.NOT_FOUND_COUPON.getMessage()); } - @DisplayName("특정 쿠폰을 조회한다. - CouponResponse") + @DisplayName("특정 쿠폰을 성공적으로 조회한다. - CouponResponse") @Test - void getById() { + void getById_success() { // Given Coupon coupon = CouponFixture.coupon(10, 100); given(couponRepository.findById(any(Long.class))).willReturn(Optional.of(coupon)); @@ -221,10 +244,10 @@ void getById_NotFoundException() { .hasMessage(ErrorMessage.NOT_FOUND_COUPON.getMessage()); } - @DisplayName("모든 쿠폰을 조회한다. - List") + @DisplayName("모든 쿠폰을 성공적으로 조회한다. - List") @MethodSource("com.moabam.support.fixture.CouponFixture#provideCoupons") @ParameterizedTest - void getAllByStatus(List coupons) { + void getAllByStatus_success(List coupons) { // Given CouponStatusRequest request = CouponFixture.couponStatusRequest(false, false); @@ -238,49 +261,4 @@ void getAllByStatus(List coupons) { // Then assertThat(actual).hasSize(coupons.size()); } - - @DisplayName("해당 쿠폰은 발급 가능 기간입니다. - Coupon") - @Test - void validatePeriod() { - // Given - LocalDateTime now = LocalDateTime.of(2023, 1, 1, 1, 0); - Coupon coupon = CouponFixture.coupon("couponName", 1, 2); - given(couponRepository.findByName(any(String.class))).willReturn(Optional.of(coupon)); - given(clockHolder.times()).willReturn(now); - - // When - Coupon actual = couponService.validatePeriod(coupon.getName()); - - // Then - assertThat(actual.getName()).isEqualTo(coupon.getName()); - } - - @DisplayName("해당 쿠폰은 발급 가능 기간이 아닙니다. - BadRequestException") - @Test - void validatePeriod_BadRequestException() { - // Given - LocalDateTime now = LocalDateTime.of(2022, 1, 1, 1, 0); - Coupon coupon = CouponFixture.coupon("couponName", 1, 2); - given(couponRepository.findByName(any(String.class))).willReturn(Optional.of(coupon)); - given(clockHolder.times()).willReturn(now); - - // When & Then - assertThatThrownBy(() -> couponService.validatePeriod("couponName")) - .isInstanceOf(BadRequestException.class) - .hasMessage(ErrorMessage.INVALID_COUPON_PERIOD.getMessage()); - } - - @DisplayName("해당 쿠폰은 존재하지 않습니다. - NotFoundException") - @Test - void validatePeriod_NotFoundException() { - // Given - LocalDateTime now = LocalDateTime.of(2022, 1, 1, 1, 0); - given(couponRepository.findByName(any(String.class))).willReturn(Optional.empty()); - given(clockHolder.times()).willReturn(now); - - // When & Then - assertThatThrownBy(() -> couponService.validatePeriod("Not found coupon name")) - .isInstanceOf(NotFoundException.class) - .hasMessage(ErrorMessage.NOT_FOUND_COUPON.getMessage()); - } } diff --git a/src/test/java/com/moabam/api/domain/coupon/CouponWalletTest.java b/src/test/java/com/moabam/api/domain/coupon/CouponWalletTest.java index d1b7ee46..059a9838 100644 --- a/src/test/java/com/moabam/api/domain/coupon/CouponWalletTest.java +++ b/src/test/java/com/moabam/api/domain/coupon/CouponWalletTest.java @@ -16,10 +16,7 @@ void couponWallet() { Coupon coupon = CouponFixture.coupon("CouponName", 1, 2); // When - CouponWallet actual = CouponWallet.builder() - .memberId(1L) - .coupon(coupon) - .build(); + CouponWallet actual = CouponWallet.create(1L, coupon); // Then assertThat(actual.getMemberId()).isEqualTo(1L); diff --git a/src/test/java/com/moabam/api/domain/coupon/repository/CouponManageRepositoryTest.java b/src/test/java/com/moabam/api/domain/coupon/repository/CouponManageRepositoryTest.java new file mode 100644 index 00000000..03c7cf49 --- /dev/null +++ b/src/test/java/com/moabam/api/domain/coupon/repository/CouponManageRepositoryTest.java @@ -0,0 +1,172 @@ +package com.moabam.api.domain.coupon.repository; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.*; + +import java.util.Set; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.redis.core.ZSetOperations.TypedTuple; + +import com.moabam.api.infrastructure.redis.StringRedisRepository; +import com.moabam.api.infrastructure.redis.ZSetRedisRepository; + +@ExtendWith(MockitoExtension.class) +class CouponManageRepositoryTest { + + @InjectMocks + CouponManageRepository couponManageRepository; + + @Mock + ZSetRedisRepository zSetRedisRepository; + + @Mock + StringRedisRepository stringRedisRepository; + + @DisplayName("쿠폰 대기열에 사용자가 성공적으로 등록된다. - Void") + @Test + void addIfAbsentQueue_success() { + // When + couponManageRepository.addIfAbsentQueue("couponName", 1L, 1); + + // Then + verify(zSetRedisRepository).addIfAbsent(any(String.class), any(Long.class), any(double.class)); + } + + @DisplayName("쿠폰명이 Null인 대기열에 사용자를 등록한다.- NullPointerException") + @Test + void addIfAbsentQueue_couponName_NullPointerException() { + // When & Then + assertThatThrownBy(() -> couponManageRepository.addIfAbsentQueue(null, 1L, 1)) + .isInstanceOf(NullPointerException.class); + } + + @DisplayName("쿠폰 대기열에 사용자 ID가 Null인 사용자를 등록한다. - NullPointerException") + @Test + void addIfAbsentQueue_memberId_NullPointerException() { + // When & Then + assertThatThrownBy(() -> couponManageRepository.addIfAbsentQueue("couponName", null, 1)) + .isInstanceOf(NullPointerException.class); + } + + @DisplayName("쿠폰 대기열에서 10명을 꺼내고 삭제한다.") + @MethodSource("com.moabam.support.fixture.CouponFixture#provideTypedTuples") + @ParameterizedTest + void popMinQueue_success(Set> tuples) { + // Given + given(zSetRedisRepository.popMin(any(String.class), any(long.class))).willReturn(tuples); + + // When + Set actual = couponManageRepository.popMinQueue("couponName", 10); + + // Then + assertThat(actual).hasSize(10); + } + + @DisplayName("쿠폰명이 Null인 대기열에서 사용자를 꺼낸다. - NullPointerException") + @Test + void popMinQueue_NullPointerException() { + // When & Then + assertThatThrownBy(() -> couponManageRepository.popMinQueue(null, 10)) + .isInstanceOf(NullPointerException.class); + } + + @DisplayName("쿠폰 대기열을 성공적으로 삭제한다. - Void") + @Test + void deleteQueue_success() { + // When + couponManageRepository.deleteQueue("couponName"); + + // Then + verify(stringRedisRepository).delete(any(String.class)); + } + + @DisplayName("쿠폰명이 Null인 대기열을 삭제한다. - NullPointerException") + @Test + void deleteQueue_NullPointerException() { + // When & Then + assertThatThrownBy(() -> couponManageRepository.deleteQueue(null)) + .isInstanceOf(NullPointerException.class); + } + + @DisplayName("쿠폰의 할당된 재고를 성공적으로 증가시킨다. - int") + @Test + void increaseIssuedStock_success() { + // Given + given(stringRedisRepository.increment(any(String.class))).willReturn(77L); + + // When + int actual = couponManageRepository.increaseIssuedStock("couponName"); + + // Then + assertThat(actual).isEqualTo(77); + } + + @DisplayName("쿠폰명이 Null인 쿠폰의 할당된 재고를 증가시킨다. - NullPointerException") + @Test + void increaseIssuedStock_NullPointerException() { + // When & Then + assertThatThrownBy(() -> couponManageRepository.increaseIssuedStock(null)) + .isInstanceOf(NullPointerException.class); + } + + @DisplayName("쿠폰의 현재 할당된 재고를 성공적으로 조회한다. - int") + @Test + void getIssuedStock_success() { + // Given + given(stringRedisRepository.get(any(String.class))).willReturn("1"); + + // When + int actual = couponManageRepository.getIssuedStock("couponName"); + + // Then + assertThat(actual).isEqualTo(1); + } + + @DisplayName("쿠폰의 현재 할당된 재고가 없어서 0이 조회된다. - int") + @Test + void getIssuedStock_zero() { + // Given + given(stringRedisRepository.get(any(String.class))).willReturn(null); + + // When + int actual = couponManageRepository.getIssuedStock("couponName"); + + // Then + assertThat(actual).isZero(); + } + + @DisplayName("쿠폰명이 Null인 쿠폰의 할당된 재고를 조회한다. - NullPointerException") + @Test + void getIssuedStock_NullPointerException() { + // When & Then + assertThatThrownBy(() -> couponManageRepository.getIssuedStock(null)) + .isInstanceOf(NullPointerException.class); + } + + @DisplayName("할당된 쿠폰 재고를 성공적으로 삭제한다. - Void") + @Test + void deleteIssuedStock_success() { + // When + couponManageRepository.deleteIssuedStock("couponName"); + + // Then + verify(stringRedisRepository).delete(any(String.class)); + } + + @DisplayName("쿠폰명이 Null인 할당된 쿠폰 재고를 삭제한다. - NullPointerException") + @Test + void deleteIssuedStock_NullPointerException() { + // When & Then + assertThatThrownBy(() -> couponManageRepository.deleteIssuedStock(null)) + .isInstanceOf(NullPointerException.class); + } +} diff --git a/src/test/java/com/moabam/api/domain/coupon/repository/CouponQueueRepositoryTest.java b/src/test/java/com/moabam/api/domain/coupon/repository/CouponQueueRepositoryTest.java deleted file mode 100644 index 9c48c266..00000000 --- a/src/test/java/com/moabam/api/domain/coupon/repository/CouponQueueRepositoryTest.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.moabam.api.domain.coupon.repository; - -import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.BDDMockito.*; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import com.moabam.api.infrastructure.redis.ZSetRedisRepository; - -@ExtendWith(MockitoExtension.class) -class CouponQueueRepositoryTest { - - @InjectMocks - private CouponQueueRepository couponQueueRepository; - - @Mock - private ZSetRedisRepository zSetRedisRepository; - - @DisplayName("특정 쿠폰의 대기열에 사용자가 성공적으로 등록된다. - Void") - @Test - void addQueue() { - // When - couponQueueRepository.addIfAbsent("couponName", "memberNickname", 1); - - // Then - verify(zSetRedisRepository).addIfAbsent(any(String.class), any(String.class), any(Double.class)); - } - - @DisplayName("특정 쿠폰의 대기열에 사용자 등록 시, 필요한 값이 NULL 이다.- NullPointerException") - @Test - void addQueue_NullPointerException() { - // When & Then - assertThatThrownBy(() -> couponQueueRepository.addIfAbsent(null, "value", 1)) - .isInstanceOf(NullPointerException.class); - } - - @DisplayName("특정 쿠폰을 발급한 사용자가 3명이다. - Long") - @Test - void queueSize() { - // Given - given(zSetRedisRepository.size(any(String.class))).willReturn(3L); - - // When - long actual = couponQueueRepository.size("key"); - - // Then - assertThat(actual).isEqualTo(3); - } - - @DisplayName("특정 쿠폰을 발급한 사용자 수 조회 시, 필요한 값이 Null이다. - NullPointerException") - @Test - void queueSize_NullPointerException() { - // When & Then - assertThatThrownBy(() -> couponQueueRepository.size(null)) - .isInstanceOf(NullPointerException.class); - } -} diff --git a/src/test/java/com/moabam/api/domain/coupon/repository/CouponWalletSearchRepositoryTest.java b/src/test/java/com/moabam/api/domain/coupon/repository/CouponWalletSearchRepositoryTest.java index f1066346..db5176c7 100644 --- a/src/test/java/com/moabam/api/domain/coupon/repository/CouponWalletSearchRepositoryTest.java +++ b/src/test/java/com/moabam/api/domain/coupon/repository/CouponWalletSearchRepositoryTest.java @@ -30,7 +30,7 @@ void find_by_id_and_member_id() { Long id = 1L; Long memberId = 1L; Coupon coupon = couponRepository.save(discount1000Coupon()); - couponWalletRepository.save(couponWallet(memberId, coupon)); + couponWalletRepository.save(CouponWallet.create(memberId, coupon)); // when CouponWallet actual = couponWalletSearchRepository.findByIdAndMemberId(id, memberId).orElseThrow(); diff --git a/src/test/java/com/moabam/api/infrastructure/redis/StringRedisRepositoryTest.java b/src/test/java/com/moabam/api/infrastructure/redis/StringRedisRepositoryTest.java index e38fbe34..83b9d44a 100644 --- a/src/test/java/com/moabam/api/infrastructure/redis/StringRedisRepositoryTest.java +++ b/src/test/java/com/moabam/api/infrastructure/redis/StringRedisRepositoryTest.java @@ -17,10 +17,11 @@ class StringRedisRepositoryTest { @Autowired - private StringRedisRepository stringRedisRepository; + StringRedisRepository stringRedisRepository; String key = "key"; String value = "value"; + String stockKey = "key_INCR"; Duration duration = Duration.ofMillis(5000); @BeforeEach @@ -30,29 +31,25 @@ void setUp() { @AfterEach void setDown() { - stringRedisRepository.delete(key); + if (stringRedisRepository.hasKey(key)) { + stringRedisRepository.delete(key); + } + + if (stringRedisRepository.hasKey(stockKey)) { + stringRedisRepository.delete(stockKey); + } } @DisplayName("레디스에 문자열 데이터가 성공적으로 저장된다. - Void") @Test - void stringRedisRepository_save() { + void save_success() { // Then assertThat(stringRedisRepository.get(key)).isEqualTo(value); } - @DisplayName("레디스의 특정 데이터가 성공적으로 삭제된다. - Void") - @Test - void stringRedisRepository_delete() { - // When - stringRedisRepository.delete(key); - - // Then - assertThat(stringRedisRepository.hasKey(key)).isFalse(); - } - @DisplayName("레디스의 특정 데이터가 성공적으로 조회된다. - String(Value)") @Test - void stringRedisRepository_get() { + void get_success() { // When String actual = stringRedisRepository.get(key); @@ -62,8 +59,28 @@ void stringRedisRepository_get() { @DisplayName("레디스의 특정 데이터 존재 여부를 성공적으로 체크한다. - Boolean") @Test - void stringRedisRepository_hasKey() { + void hasKey_success() { // When & Then assertThat(stringRedisRepository.hasKey("not found key")).isFalse(); } + + @DisplayName("레디스의 특정 데이터가 성공적으로 삭제된다. - Void") + @Test + void delete_success() { + // When + stringRedisRepository.delete(key); + + // Then + assertThat(stringRedisRepository.hasKey(key)).isFalse(); + } + + @DisplayName("레디스의 특정 데이터의 값이 1 증가한다.") + @Test + void increment_success() { + // When + Long actual = stringRedisRepository.increment(stockKey); + + // Then + assertThat(actual).isEqualTo(1L); + } } diff --git a/src/test/java/com/moabam/api/infrastructure/redis/ZSetRedisRepositoryTest.java b/src/test/java/com/moabam/api/infrastructure/redis/ZSetRedisRepositoryTest.java index 0713fdff..5b94879d 100644 --- a/src/test/java/com/moabam/api/infrastructure/redis/ZSetRedisRepositoryTest.java +++ b/src/test/java/com/moabam/api/infrastructure/redis/ZSetRedisRepositoryTest.java @@ -2,42 +2,49 @@ import static org.assertj.core.api.Assertions.*; +import java.util.Set; + import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ZSetOperations.TypedTuple; import com.moabam.global.config.EmbeddedRedisConfig; -@SpringBootTest(classes = {EmbeddedRedisConfig.class, ZSetRedisRepository.class}) +@SpringBootTest(classes = {EmbeddedRedisConfig.class, ZSetRedisRepository.class, StringRedisRepository.class}) class ZSetRedisRepositoryTest { @Autowired - private ZSetRedisRepository zSetRedisRepository; + ZSetRedisRepository zSetRedisRepository; + + @Autowired + StringRedisRepository stringRedisRepository; @Autowired - private RedisTemplate redisTemplate; + RedisTemplate redisTemplate; String key = "key"; - String value = "value"; + Long value = 1L; @AfterEach void afterEach() { - if (zSetRedisRepository.hasKey(key)) { - zSetRedisRepository.delete(key); + if (stringRedisRepository.hasKey(key)) { + stringRedisRepository.delete(key); } } @DisplayName("레디스의 SortedSet 데이터가 성공적으로 저장된다. - Void") @Test - void setRedisRepository_addIfAbsent() { + void addIfAbsent_success() { // When zSetRedisRepository.addIfAbsent(key, value, 1); // Then - assertThat(zSetRedisRepository.size(key)).isEqualTo(1); + assertThat(stringRedisRepository.hasKey(key)).isTrue(); } @DisplayName("이미 존재하는 값을 한 번 더 저장을 시도한다. - Void") @@ -51,36 +58,54 @@ void setRedisRepository_addIfAbsent_not_update() { assertThat(redisTemplate.opsForZSet().score(key, value)).isEqualTo(1); } - @DisplayName("레디스의 특정 키의 사이즈가 성공적으로 반환된다. - int") + @Disabled + @DisplayName("저장된 데이터와 동일한 갯수만큼의 반환과 삭제를 성공적으로 시도한다. - Set>") @Test - void setRedisRepository_size() { + void popMin_same_success() { // Given - zSetRedisRepository.addIfAbsent(key, value, 1); + zSetRedisRepository.addIfAbsent(key, value + 1, 1); + zSetRedisRepository.addIfAbsent(key, value + 2, 2); + zSetRedisRepository.addIfAbsent(key, value + 3, 3); // When - long actual = zSetRedisRepository.size(key); + Set> actual = zSetRedisRepository.popMin(key, 3); // Then - assertThat(actual).isEqualTo(1); + assertThat(actual).hasSize(3); + assertThat(stringRedisRepository.hasKey(key)).isFalse(); } - @DisplayName("레디스의 특정 데이터가 성공적으로 삭제된다. - Void") + @Disabled + @DisplayName("저장된 데이터보다 많은 갯수만큼의 반환과 삭제를 성공적으로 시도한다. - Set>") @Test - void setRedisRepository_delete() { + void popMin_more_success() { // Given - zSetRedisRepository.addIfAbsent(key, value, 1); + zSetRedisRepository.addIfAbsent(key, value + 1, 1); + zSetRedisRepository.addIfAbsent(key, value + 2, 2); // When - zSetRedisRepository.delete(key); + Set> actual = zSetRedisRepository.popMin(key, 3); // Then - assertThat(zSetRedisRepository.hasKey(key)).isFalse(); + assertThat(actual).hasSize(2); } - @DisplayName("레디스의 특정 데이터 존재 여부를 성공적으로 체크한다. - Boolean") + @Disabled + @DisplayName("저장된 데이터보다 더 적은 갯수만큼의 반환과 삭제를 성공적으로 시도한다. - Set>") @Test - void setRedisRepository_hasKey() { - // When & Then - assertThat(zSetRedisRepository.hasKey("not found key")).isFalse(); + void popMin_less_success() { + // Given + zSetRedisRepository.addIfAbsent(key, value + 1, 1); + zSetRedisRepository.addIfAbsent(key, value + 2, 2); + zSetRedisRepository.addIfAbsent(key, value + 3, 3); + zSetRedisRepository.addIfAbsent(key, value + 4, 4); + zSetRedisRepository.addIfAbsent(key, value + 5, 5); + + // When + Set> actual = zSetRedisRepository.popMin(key, 3); + + // Then + assertThat(actual).hasSize(3); + assertThat(stringRedisRepository.hasKey(key)).isTrue(); } } diff --git a/src/test/java/com/moabam/api/presentation/CouponControllerTest.java b/src/test/java/com/moabam/api/presentation/CouponControllerTest.java index 87173012..d9fa0bfa 100644 --- a/src/test/java/com/moabam/api/presentation/CouponControllerTest.java +++ b/src/test/java/com/moabam/api/presentation/CouponControllerTest.java @@ -47,21 +47,21 @@ class CouponControllerTest extends WithoutFilterSupporter { @Autowired - private MockMvc mockMvc; + MockMvc mockMvc; @Autowired - private ObjectMapper objectMapper; + ObjectMapper objectMapper; @Autowired - private CouponRepository couponRepository; + CouponRepository couponRepository; @MockBean - private ClockHolder clockHolder; + ClockHolder clockHolder; @WithMember(role = Role.ADMIN) @DisplayName("POST - 쿠폰을 성공적으로 발행한다. - Void") @Test - void create_Coupon() throws Exception { + void create_Coupon_success() throws Exception { // Given CreateCouponRequest request = CouponFixture.createCouponRequest(); given(clockHolder.times()).willReturn(LocalDateTime.of(2022, 1, 1, 1, 1)); @@ -130,7 +130,7 @@ void create_Coupon_OpenAt_BadRequestException() throws Exception { @WithMember(role = Role.ADMIN) @DisplayName("POST - 쿠폰명이 중복된 쿠폰을 발행한다. - ConflictException") @Test - void create_Coupon_ConflictException() throws Exception { + void create_Coupon_Name_ConflictException() throws Exception { // Given CreateCouponRequest request = CouponFixture.createCouponRequest(); couponRepository.save(CouponMapper.toEntity(1L, request)); @@ -150,10 +150,34 @@ void create_Coupon_ConflictException() throws Exception { .andExpect(jsonPath("$.message").value(ErrorMessage.CONFLICT_COUPON_NAME.getMessage())); } + @WithMember(role = Role.ADMIN) + @DisplayName("POST - 쿠폰 발행 가능 날짜가 중복된 쿠폰을 발행한다. - ConflictException") + @Test + void create_Coupon_StartAt_ConflictException() throws Exception { + // Given + CreateCouponRequest request = CouponFixture.createCouponRequest(); + Coupon conflictStartAtCoupon = CouponFixture.coupon("NotConflictName", 2, 1); + couponRepository.save(conflictStartAtCoupon); + + // When & Then + mockMvc.perform(post("/admins/coupons") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andDo(document("admins/coupons", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + CouponSnippetFixture.CREATE_COUPON_REQUEST, + ErrorSnippetFixture.ERROR_MESSAGE_RESPONSE)) + .andExpect(status().isConflict()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.message").value(ErrorMessage.CONFLICT_COUPON_START_AT.getMessage())); + } + @WithMember(role = Role.ADMIN) @DisplayName("DELETE - 쿠폰을 성공적으로 삭제한다. - Void") @Test - void delete_Coupon() throws Exception { + void delete_Coupon_success() throws Exception { // Given Coupon coupon = couponRepository.save(CouponFixture.coupon(10, 100)); @@ -182,9 +206,9 @@ void delete_Coupon_NotFoundException() throws Exception { .andExpect(jsonPath("$.message").value(ErrorMessage.NOT_FOUND_COUPON.getMessage())); } - @DisplayName("GET - 특정 쿠폰을 조회한다. - CouponResponse") + @DisplayName("GET - 특정 쿠폰을 성공적으로 조회한다. - CouponResponse") @Test - void getById_Coupon() throws Exception { + void getById_Coupon_success() throws Exception { // Given Coupon coupon = couponRepository.save(CouponFixture.coupon(10, 100)); @@ -218,7 +242,7 @@ void getById_Coupon_NotFoundException() throws Exception { @DisplayName("POST - 모든 쿠폰을 조회한다. - List") @MethodSource("com.moabam.support.fixture.CouponFixture#provideCoupons") @ParameterizedTest - void getAllByStatus_Coupons(List coupons) throws Exception { + void getAllByStatus_Coupons_success(List coupons) throws Exception { // Given CouponStatusRequest request = CouponFixture.couponStatusRequest(true, true); List coupon = couponRepository.saveAll(coupons); @@ -240,10 +264,10 @@ void getAllByStatus_Coupons(List coupons) throws Exception { .andExpect(jsonPath("$", hasSize(coupon.size()))); } - @DisplayName("POST - 발급 가능한 쿠폰만 조회한다.. - List") + @DisplayName("POST - 발급 가능한 쿠폰만 조회한다. - List") @MethodSource("com.moabam.support.fixture.CouponFixture#provideCoupons") @ParameterizedTest - void getAllByStatus_Coupon(List coupons) throws Exception { + void getAllByStatus_Coupon_success(List coupons) throws Exception { // Given CouponStatusRequest request = CouponFixture.couponStatusRequest(false, false); couponRepository.saveAll(coupons); @@ -264,10 +288,10 @@ void getAllByStatus_Coupon(List coupons) throws Exception { .andExpect(jsonPath("$", hasSize(1))); } - @WithMember(nickname = "member-coupon-1") - @DisplayName("POST - 쿠폰 발급 요청을 한다. - Void") + @WithMember + @DisplayName("POST - 쿠폰 발급 요청을 성공적으로 한다. - Void") @Test - void registerQueue() throws Exception { + void registerQueue_success() throws Exception { // Given Coupon couponFixture = CouponFixture.coupon(); Coupon coupon = couponRepository.save(couponFixture); @@ -284,15 +308,15 @@ void registerQueue() throws Exception { .andExpect(status().isOk()); } - @WithMember(nickname = "member-coupon-2") - @DisplayName("POST - 발급 기간이 아닌 쿠폰에 발급 요청을 한다. - BadRequestException") + @WithMember + @DisplayName("POST - 발급이 가능한 쿠폰이 없는 상황에 쿠폰 발급 요청을 한다. - BadRequestException") @Test - void registerQueue_BadRequestException() throws Exception { + void registerQueue_Zero_StartAt_BadRequestException() throws Exception { // Given Coupon couponFixture = CouponFixture.coupon(); Coupon coupon = couponRepository.save(couponFixture); - given(clockHolder.times()).willReturn(LocalDateTime.of(2022, 2, 1, 1, 1)); + given(clockHolder.times()).willReturn(LocalDateTime.of(2022, 1, 1, 1, 1)); // When & Then mockMvc.perform(post("/coupons") @@ -307,12 +331,35 @@ void registerQueue_BadRequestException() throws Exception { .andExpect(jsonPath("$.message").value(ErrorMessage.INVALID_COUPON_PERIOD.getMessage())); } + @WithMember + @DisplayName("POST - 발급 기간이 아닌 쿠폰에 발급 요청을 한다. - BadRequestException") + @Test + void registerQueue_Not_StartAt_BadRequestException() throws Exception { + // Given + Coupon couponFixture = CouponFixture.coupon(); + couponRepository.save(couponFixture); + + given(clockHolder.times()).willReturn(LocalDateTime.of(2022, 2, 1, 1, 1)); + + // When & Then + mockMvc.perform(post("/coupons") + .param("couponName", "not start couponName")) + .andDo(print()) + .andDo(document("coupons", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + ErrorSnippetFixture.ERROR_MESSAGE_RESPONSE)) + .andExpect(status().isBadRequest()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.message").value(ErrorMessage.INVALID_COUPON_PERIOD.getMessage())); + } + @WithMember @DisplayName("POST - 존재하지 않는 쿠폰에 발급 요청을 한다. - NotFoundException") @Test void registerQueue_NotFoundException() throws Exception { // Given - Coupon coupon = CouponFixture.coupon("Not found coupon name", 2, 1); + Coupon coupon = CouponFixture.coupon("Not found couponName", 2, 1); given(clockHolder.times()).willReturn(LocalDateTime.of(2023, 2, 1, 1, 1)); @@ -324,8 +371,8 @@ void registerQueue_NotFoundException() throws Exception { preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), ErrorSnippetFixture.ERROR_MESSAGE_RESPONSE)) - .andExpect(status().isNotFound()) + .andExpect(status().isBadRequest()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$.message").value(ErrorMessage.NOT_FOUND_COUPON.getMessage())); + .andExpect(jsonPath("$.message").value(ErrorMessage.INVALID_COUPON_PERIOD.getMessage())); } } diff --git a/src/test/java/com/moabam/support/fixture/CouponFixture.java b/src/test/java/com/moabam/support/fixture/CouponFixture.java index 9d02a077..647eecaa 100644 --- a/src/test/java/com/moabam/support/fixture/CouponFixture.java +++ b/src/test/java/com/moabam/support/fixture/CouponFixture.java @@ -1,14 +1,17 @@ package com.moabam.support.fixture; import java.time.LocalDate; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.stream.Stream; import org.junit.jupiter.params.provider.Arguments; +import org.springframework.data.redis.core.DefaultTypedTuple; +import org.springframework.data.redis.core.ZSetOperations; import com.moabam.api.domain.coupon.Coupon; import com.moabam.api.domain.coupon.CouponType; -import com.moabam.api.domain.coupon.CouponWallet; import com.moabam.api.dto.coupon.CouponStatusRequest; import com.moabam.api.dto.coupon.CreateCouponRequest; @@ -77,13 +80,6 @@ public static Coupon discount10000Coupon() { .build(); } - public static CouponWallet couponWallet(Long memberId, Coupon coupon) { - return CouponWallet.builder() - .memberId(memberId) - .coupon(coupon) - .build(); - } - public static CreateCouponRequest createCouponRequest() { return CreateCouponRequest.builder() .name("couponName") @@ -131,4 +127,20 @@ public static Stream provideCoupons() { )) ); } + + public static Stream provideTypedTuples() { + Set> tuples = new HashSet<>(); + tuples.add(new DefaultTypedTuple<>(1L, 1.0)); + tuples.add(new DefaultTypedTuple<>(2L, 2.0)); + tuples.add(new DefaultTypedTuple<>(3L, 3.0)); + tuples.add(new DefaultTypedTuple<>(4L, 4.0)); + tuples.add(new DefaultTypedTuple<>(5L, 5.0)); + tuples.add(new DefaultTypedTuple<>(6L, 6.0)); + tuples.add(new DefaultTypedTuple<>(7L, 7.0)); + tuples.add(new DefaultTypedTuple<>(8L, 8.0)); + tuples.add(new DefaultTypedTuple<>(9L, 9.0)); + tuples.add(new DefaultTypedTuple<>(10L, 10.0)); + + return Stream.of(Arguments.of(tuples)); + } }