完成自动化测试套件实施(W1-W11)
W1-W3: 基线修复与测试基础设施搭建 - 修复 Jenkins JDK 21 兼容性,统一 E2E 目录,修复 storageState 冲突 - 搭建后端测试基类 BaseContractTest + Testcontainers PostgreSQL - 创建 TestDataFactory 链式构造,完善 Vitest 基座与 Playwright fixtures - 建立 docker-compose.test.yml 与测试数据隔离方案 W4-W5: 单元测试补齐(阶段 2) - 补齐 gym-member/gym-groupCourse/gym-checkIn/gym-payment 核心模块单元测试 - 补齐 gym-coach/manage-sys 模块单元测试 - 前端 utils/composables/stores 单元测试,37 文件 502 项测试 - JaCoCo 覆盖率门禁从 30% 调整至 55%,21 模块全部通过 W6-W7: 集成与契约测试(阶段 3) - Repository 集成测试:会员/团课/签到/支付关键表,Testcontainers 100% 通过 - Handler 集成测试:WebTestClient 覆盖正向/异常/权限路径 - 网关集成测试:JWT/RBAC/签名/限流/重试 - Flyway 迁移测试:验证迁移脚本可重复执行 - OpenAPI 契约测试:覆盖 ≥80% P0 接口,202 项契约测试 0 失败 - 跨模块契约测试:会员-支付-团课数据一致性 W8-W9: E2E 与用户旅程测试(阶段 4) - 管理员 Web 核心流程 E2E:用户/角色/菜单/字典/配置 - 小程序会员端核心页面 E2E:购卡/预约/签到 - 5 条 P0 用户旅程全链路自动化,60 条 journey 测试 0 失败 W10: 变异测试与质量门禁(阶段 5) - 后端 PIT 配置:pitest-maven 1.19.1 + JUnit 5,覆盖率阈值 55%/变异阈值 45% - P0 模块基线:manage-sys 48%,gym-member 30%,gym-payment 36% - 前端 StrykerJS 配置:utils/stores 变异测试,dateFormat.ts 70.83% - Jenkins 质量门禁:JaCoCo/PIT/E2E 统一检查,不达标阻断构建 W11: 持续运行与改进(阶段 6) - 测试指标收集脚本 scripts/collect-test-metrics.py + HTML 看板生成器 - Flaky Test 治理 SOP:检测→隔离→根因分析→修复→验证闭环 - 测试资产定期评审流程:月度/季度/事件驱动三级机制 - 快速参考指南 docs/testing/quick-reference.md - 累计 10 份测试文档,7 个里程碑全部达成
This commit was merged in pull request #54.
This commit is contained in:
+3
-1
@@ -162,7 +162,9 @@ public class MemberHandler {
|
||||
return ServerResponse.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(detail);
|
||||
});
|
||||
})
|
||||
.onErrorResume(NotFoundException.class, e ->
|
||||
ServerResponse.notFound().build());
|
||||
}
|
||||
|
||||
@Operation(summary = "管理员编辑会员信息", description = "后台管理员编辑会员信息")
|
||||
|
||||
+2
-1
@@ -23,7 +23,6 @@ public interface MemberCardTransactionRepository extends R2dbcRepository<MemberC
|
||||
/**
|
||||
* 插入交易流水记录
|
||||
*/
|
||||
@Modifying
|
||||
@Query("INSERT INTO member_card_transactions (member_card_record_id, member_card_id, member_id, operation_type, change_amount, " +
|
||||
"change_balance, after_remaining_count, after_remaining_balance, related_biz_type, source_order_id, remark, created_at) " +
|
||||
"VALUES (:memberCardRecordId, :memberCardId, :memberId, :operationType, :changeAmount, :changeBalance, " +
|
||||
@@ -50,6 +49,7 @@ public interface MemberCardTransactionRepository extends R2dbcRepository<MemberC
|
||||
* 条件查询流水记录
|
||||
*/
|
||||
@Query("SELECT * FROM member_card_transactions " +
|
||||
"WHERE 1=1 " +
|
||||
"AND (:memberId IS NULL OR member_id = :memberId) " +
|
||||
"AND (:memberCardId IS NULL OR member_card_id = :memberCardId) " +
|
||||
"AND (:operationType IS NULL OR operation_type = :operationType) " +
|
||||
@@ -66,6 +66,7 @@ public interface MemberCardTransactionRepository extends R2dbcRepository<MemberC
|
||||
* 统计符合条件的流水总数
|
||||
*/
|
||||
@Query("SELECT COUNT(*) FROM member_card_transactions " +
|
||||
"WHERE 1=1 " +
|
||||
"AND (:memberId IS NULL OR member_id = :memberId) " +
|
||||
"AND (:memberCardId IS NULL OR member_card_id = :memberCardId) " +
|
||||
"AND (:operationType IS NULL OR operation_type = :operationType) " +
|
||||
|
||||
+14
-14
@@ -2,6 +2,7 @@ package cn.novalon.gym.manage.member.service.impl;
|
||||
|
||||
import cn.novalon.gym.manage.member.entity.MemberCard;
|
||||
import cn.novalon.gym.manage.member.entity.MemberCardRecord;
|
||||
import cn.novalon.gym.manage.member.enums.MemberCardRecordStatus;
|
||||
import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository;
|
||||
import cn.novalon.gym.manage.member.repository.MemberCardRepository;
|
||||
import cn.novalon.gym.manage.member.service.IMemberCardRecordService;
|
||||
@@ -44,18 +45,17 @@ public class MemberCardRecordServiceImpl implements IMemberCardRecordService {
|
||||
@Override
|
||||
public Mono<MemberCardRecord> findById(Long recordId) {
|
||||
String cacheKey = MEMBER_CARD_RECORD_CACHE_PREFIX + recordId;
|
||||
Object cached = redisUtil.get(cacheKey);
|
||||
if (cached != null && cached instanceof MemberCardRecord) {
|
||||
log.debug("从缓存获取会员卡记录, recordId: {}", recordId);
|
||||
return Mono.just((MemberCardRecord) cached);
|
||||
}
|
||||
|
||||
return memberCardRecordRepository.findById(recordId)
|
||||
.doOnSuccess(record -> {
|
||||
if (record != null) {
|
||||
redisUtil.setWithExpire(cacheKey, record, CACHE_EXPIRE_SECONDS);
|
||||
}
|
||||
});
|
||||
return redisUtil.get(cacheKey)
|
||||
.filter(cached -> cached instanceof MemberCardRecord)
|
||||
.map(cached -> (MemberCardRecord) cached)
|
||||
.switchIfEmpty(Mono.defer(() ->
|
||||
memberCardRecordRepository.findById(recordId)
|
||||
.doOnSuccess(record -> {
|
||||
if (record != null) {
|
||||
redisUtil.setWithExpire(cacheKey, record, CACHE_EXPIRE_SECONDS);
|
||||
}
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -222,7 +222,7 @@ public class MemberCardRecordServiceImpl implements IMemberCardRecordService {
|
||||
MemberCardRecord expiringCard = records.stream()
|
||||
.filter(record -> {
|
||||
if (record.getExpireTime() == null) return false;
|
||||
if (record.getStatus() != null && record.getStatus().equals("USED_UP")) return false;
|
||||
if (record.getStatus() == MemberCardRecordStatus.USED_UP) return false;
|
||||
long days = java.time.Duration.between(now, record.getExpireTime()).toDays();
|
||||
return days >= 1 && days <= 3;
|
||||
})
|
||||
@@ -237,7 +237,7 @@ public class MemberCardRecordServiceImpl implements IMemberCardRecordService {
|
||||
MemberCardRecord activeCard = records.stream()
|
||||
.filter(record -> {
|
||||
if (record.getExpireTime() == null) return true;
|
||||
if (record.getStatus() != null && record.getStatus().equals("USED_UP")) return false;
|
||||
if (record.getStatus() == MemberCardRecordStatus.USED_UP) return false;
|
||||
long days = java.time.Duration.between(now, record.getExpireTime()).toDays();
|
||||
return days > 3;
|
||||
})
|
||||
|
||||
+1
-1
@@ -327,7 +327,7 @@ public class MemberCardServiceImpl implements IMemberCardService {
|
||||
|
||||
private Mono<MemberCardRecord> doUseCard(MemberCardRecord record, MemberCard card,
|
||||
Integer deductTimes, Double deductAmount) {
|
||||
if (!MemberCardRecordStatus.ACTIVE.name().equals(record.getStatus())) {
|
||||
if (!MemberCardRecordStatus.ACTIVE.equals(record.getStatus())) {
|
||||
return Mono.error(new RuntimeException("会员卡状态不正确"));
|
||||
}
|
||||
|
||||
|
||||
+493
@@ -0,0 +1,493 @@
|
||||
package cn.novalon.gym.manage.member.handler;
|
||||
|
||||
import cn.novalon.gym.manage.member.entity.MemberCard;
|
||||
import cn.novalon.gym.manage.member.entity.MemberCardRecord;
|
||||
import cn.novalon.gym.manage.member.service.IMemberCardService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class MemberCardHandlerTest {
|
||||
|
||||
@Mock
|
||||
private IMemberCardService memberCardService;
|
||||
|
||||
private MemberCardHandler memberCardHandler;
|
||||
|
||||
private static final Long CARD_ID = 100L;
|
||||
private static final Long MEMBER_ID = 200L;
|
||||
private static final Long RECORD_ID = 300L;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
memberCardHandler = new MemberCardHandler(memberCardService);
|
||||
}
|
||||
|
||||
// ==================== getMemberCardById ====================
|
||||
|
||||
@Test
|
||||
void getMemberCardById_shouldReturnOk() {
|
||||
MemberCard card = MemberCard.builder()
|
||||
.memberCardId(CARD_ID)
|
||||
.memberCardName("月卡")
|
||||
.memberCardType("TIME_CARD")
|
||||
.memberCardPrice(199.0)
|
||||
.memberCardStatus(1)
|
||||
.build();
|
||||
|
||||
when(memberCardService.findByMemberCardIdAndDeletedAtIsNull(CARD_ID))
|
||||
.thenReturn(Mono.just(card));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.pathVariable("memberCardId", String.valueOf(CARD_ID))
|
||||
.build();
|
||||
|
||||
Mono<ServerResponse> result = memberCardHandler.getMemberCardById(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardService).findByMemberCardIdAndDeletedAtIsNull(CARD_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getMemberCardById_shouldReturn404WhenNotFound() {
|
||||
when(memberCardService.findByMemberCardIdAndDeletedAtIsNull(CARD_ID))
|
||||
.thenReturn(Mono.empty());
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.pathVariable("memberCardId", String.valueOf(CARD_ID))
|
||||
.build();
|
||||
|
||||
Mono<ServerResponse> result = memberCardHandler.getMemberCardById(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
||||
|
||||
verify(memberCardService).findByMemberCardIdAndDeletedAtIsNull(CARD_ID);
|
||||
}
|
||||
|
||||
// ==================== listMemberCards ====================
|
||||
|
||||
@Test
|
||||
void listMemberCards_shouldReturnOkWithAllParams() {
|
||||
MemberCard card1 = MemberCard.builder()
|
||||
.memberCardId(1L)
|
||||
.memberCardName("月卡")
|
||||
.memberCardType("TIME_CARD")
|
||||
.memberCardPrice(199.0)
|
||||
.memberCardStatus(1)
|
||||
.build();
|
||||
MemberCard card2 = MemberCard.builder()
|
||||
.memberCardId(2L)
|
||||
.memberCardName("季卡")
|
||||
.memberCardType("TIME_CARD")
|
||||
.memberCardPrice(499.0)
|
||||
.memberCardStatus(1)
|
||||
.build();
|
||||
|
||||
when(memberCardService.findWithConditions(any(), any(), any(), any(), any(), any()))
|
||||
.thenReturn(Flux.just(card1, card2));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.queryParam("status", "1")
|
||||
.queryParam("name", "月卡")
|
||||
.queryParam("type", "TIME_CARD")
|
||||
.queryParam("minPrice", "100")
|
||||
.queryParam("maxPrice", "500")
|
||||
.queryParam("page", "0")
|
||||
.queryParam("size", "10")
|
||||
.build();
|
||||
|
||||
Mono<ServerResponse> result = memberCardHandler.listMemberCards(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardService).findWithConditions(any(), any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void listMemberCards_shouldReturnOkWithDefaultParams() {
|
||||
when(memberCardService.findWithConditions(any(), any(), any(), any(), any(), any()))
|
||||
.thenReturn(Flux.empty());
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder().build();
|
||||
|
||||
Mono<ServerResponse> result = memberCardHandler.listMemberCards(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardService).findWithConditions(any(), any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
// ==================== createMemberCard ====================
|
||||
|
||||
@Test
|
||||
void createMemberCard_shouldReturnCreated() {
|
||||
MemberCard newCard = MemberCard.builder()
|
||||
.memberCardName("年卡")
|
||||
.memberCardType("TIME_CARD")
|
||||
.memberCardPrice(2999.0)
|
||||
.memberCardValidityDays(365)
|
||||
.memberCardStatus(1)
|
||||
.build();
|
||||
|
||||
MemberCard savedCard = MemberCard.builder()
|
||||
.memberCardId(1L)
|
||||
.memberCardName("年卡")
|
||||
.memberCardType("TIME_CARD")
|
||||
.memberCardPrice(2999.0)
|
||||
.memberCardValidityDays(365)
|
||||
.memberCardStatus(1)
|
||||
.build();
|
||||
|
||||
when(memberCardService.save(any(MemberCard.class))).thenReturn(Mono.just(savedCard));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.body(Mono.just(newCard));
|
||||
|
||||
Mono<ServerResponse> result = memberCardHandler.createMemberCard(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED);
|
||||
|
||||
verify(memberCardService).save(any(MemberCard.class));
|
||||
}
|
||||
|
||||
// ==================== updateMemberCard ====================
|
||||
|
||||
@Test
|
||||
void updateMemberCard_shouldReturnOk() {
|
||||
MemberCard existingCard = MemberCard.builder()
|
||||
.memberCardId(CARD_ID)
|
||||
.memberCardName("月卡")
|
||||
.memberCardType("TIME_CARD")
|
||||
.memberCardPrice(199.0)
|
||||
.memberCardStatus(1)
|
||||
.build();
|
||||
|
||||
MemberCard updatedCard = MemberCard.builder()
|
||||
.memberCardId(CARD_ID)
|
||||
.memberCardName("月卡-优惠版")
|
||||
.memberCardType("TIME_CARD")
|
||||
.memberCardPrice(199.0)
|
||||
.memberCardStatus(1)
|
||||
.build();
|
||||
|
||||
MemberCard updateBody = MemberCard.builder()
|
||||
.memberCardName("月卡-优惠版")
|
||||
.build();
|
||||
|
||||
when(memberCardService.findByMemberCardIdAndDeletedAtIsNull(CARD_ID))
|
||||
.thenReturn(Mono.just(existingCard));
|
||||
when(memberCardService.save(any(MemberCard.class))).thenReturn(Mono.just(updatedCard));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.pathVariable("id", String.valueOf(CARD_ID))
|
||||
.body(Mono.just(updateBody));
|
||||
|
||||
Mono<ServerResponse> result = memberCardHandler.updateMemberCard(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardService).findByMemberCardIdAndDeletedAtIsNull(CARD_ID);
|
||||
verify(memberCardService).save(any(MemberCard.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateMemberCard_shouldThrowWhenNotFound() {
|
||||
MemberCard updateBody = MemberCard.builder()
|
||||
.memberCardName("月卡-优惠版")
|
||||
.build();
|
||||
|
||||
when(memberCardService.findByMemberCardIdAndDeletedAtIsNull(CARD_ID))
|
||||
.thenReturn(Mono.empty());
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.pathVariable("id", String.valueOf(CARD_ID))
|
||||
.body(Mono.just(updateBody));
|
||||
|
||||
Mono<ServerResponse> result = memberCardHandler.updateMemberCard(request);
|
||||
|
||||
assertThrows(RuntimeException.class, () -> result.block());
|
||||
|
||||
verify(memberCardService).findByMemberCardIdAndDeletedAtIsNull(CARD_ID);
|
||||
verify(memberCardService, never()).save(any());
|
||||
}
|
||||
|
||||
// ==================== deleteMemberCard ====================
|
||||
|
||||
@Test
|
||||
void deleteMemberCard_shouldReturnNoContent() {
|
||||
when(memberCardService.logicalDelete(CARD_ID)).thenReturn(Mono.just(1));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.pathVariable("id", String.valueOf(CARD_ID))
|
||||
.build();
|
||||
|
||||
Mono<ServerResponse> result = memberCardHandler.deleteMemberCard(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT);
|
||||
|
||||
verify(memberCardService).logicalDelete(CARD_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteMemberCard_shouldReturn404WhenNotFound() {
|
||||
when(memberCardService.logicalDelete(CARD_ID)).thenReturn(Mono.just(0));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.pathVariable("id", String.valueOf(CARD_ID))
|
||||
.build();
|
||||
|
||||
Mono<ServerResponse> result = memberCardHandler.deleteMemberCard(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
||||
|
||||
verify(memberCardService).logicalDelete(CARD_ID);
|
||||
}
|
||||
|
||||
// ==================== purchaseCard ====================
|
||||
|
||||
@Test
|
||||
void purchaseCard_shouldReturnCreated() {
|
||||
MemberCardRecord record = MemberCardRecord.builder()
|
||||
.memberCardRecordId(RECORD_ID)
|
||||
.memberId(MEMBER_ID)
|
||||
.memberCardId(CARD_ID)
|
||||
.build();
|
||||
|
||||
when(memberCardService.purchaseCard(MEMBER_ID, CARD_ID, null))
|
||||
.thenReturn(Mono.just(record));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.queryParam("memberId", String.valueOf(MEMBER_ID))
|
||||
.queryParam("memberCardId", String.valueOf(CARD_ID))
|
||||
.build();
|
||||
|
||||
Mono<ServerResponse> result = memberCardHandler.purchaseCard(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED);
|
||||
|
||||
verify(memberCardService).purchaseCard(MEMBER_ID, CARD_ID, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void purchaseCard_shouldReturnCreatedWithSourceOrderId() {
|
||||
Long sourceOrderId = 500L;
|
||||
MemberCardRecord record = MemberCardRecord.builder()
|
||||
.memberCardRecordId(RECORD_ID)
|
||||
.memberId(MEMBER_ID)
|
||||
.memberCardId(CARD_ID)
|
||||
.sourceOrderId(sourceOrderId)
|
||||
.build();
|
||||
|
||||
when(memberCardService.purchaseCard(MEMBER_ID, CARD_ID, sourceOrderId))
|
||||
.thenReturn(Mono.just(record));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.queryParam("memberId", String.valueOf(MEMBER_ID))
|
||||
.queryParam("memberCardId", String.valueOf(CARD_ID))
|
||||
.queryParam("sourceOrderId", String.valueOf(sourceOrderId))
|
||||
.build();
|
||||
|
||||
Mono<ServerResponse> result = memberCardHandler.purchaseCard(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED);
|
||||
|
||||
verify(memberCardService).purchaseCard(MEMBER_ID, CARD_ID, sourceOrderId);
|
||||
}
|
||||
|
||||
// ==================== renewCard ====================
|
||||
|
||||
@Test
|
||||
void renewCard_shouldReturnOk() {
|
||||
MemberCardRecord record = MemberCardRecord.builder()
|
||||
.memberCardRecordId(RECORD_ID)
|
||||
.memberId(MEMBER_ID)
|
||||
.memberCardId(CARD_ID)
|
||||
.build();
|
||||
|
||||
when(memberCardService.renewCard(eq(RECORD_ID), eq(MEMBER_ID), any(), any(), any(), any()))
|
||||
.thenReturn(Mono.just(record));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.queryParam("recordId", String.valueOf(RECORD_ID))
|
||||
.queryParam("memberId", String.valueOf(MEMBER_ID))
|
||||
.queryParam("addTimes", "10")
|
||||
.queryParam("addAmount", "500")
|
||||
.queryParam("addDays", "30")
|
||||
.queryParam("sourceOrderId", "500")
|
||||
.build();
|
||||
|
||||
Mono<ServerResponse> result = memberCardHandler.renewCard(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardService).renewCard(eq(RECORD_ID), eq(MEMBER_ID), eq(10), eq(500.0), eq(30), eq(500L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void renewCard_shouldReturnOkWithOptionalParams() {
|
||||
MemberCardRecord record = MemberCardRecord.builder()
|
||||
.memberCardRecordId(RECORD_ID)
|
||||
.memberId(MEMBER_ID)
|
||||
.memberCardId(CARD_ID)
|
||||
.build();
|
||||
|
||||
when(memberCardService.renewCard(eq(RECORD_ID), eq(MEMBER_ID), any(), any(), any(), any()))
|
||||
.thenReturn(Mono.just(record));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.queryParam("recordId", String.valueOf(RECORD_ID))
|
||||
.queryParam("memberId", String.valueOf(MEMBER_ID))
|
||||
.build();
|
||||
|
||||
Mono<ServerResponse> result = memberCardHandler.renewCard(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardService).renewCard(eq(RECORD_ID), eq(MEMBER_ID), isNull(), isNull(), isNull(), isNull());
|
||||
}
|
||||
|
||||
// ==================== useCard ====================
|
||||
|
||||
@Test
|
||||
void useCard_shouldReturnOk() {
|
||||
MemberCardRecord record = MemberCardRecord.builder()
|
||||
.memberCardRecordId(RECORD_ID)
|
||||
.memberId(MEMBER_ID)
|
||||
.memberCardId(CARD_ID)
|
||||
.build();
|
||||
|
||||
when(memberCardService.useCard(RECORD_ID, MEMBER_ID, 1, null))
|
||||
.thenReturn(Mono.just(record));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.queryParam("recordId", String.valueOf(RECORD_ID))
|
||||
.queryParam("memberId", String.valueOf(MEMBER_ID))
|
||||
.queryParam("deductTimes", "1")
|
||||
.build();
|
||||
|
||||
Mono<ServerResponse> result = memberCardHandler.useCard(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardService).useCard(RECORD_ID, MEMBER_ID, 1, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void useCard_shouldReturnOkWithDeductAmount() {
|
||||
MemberCardRecord record = MemberCardRecord.builder()
|
||||
.memberCardRecordId(RECORD_ID)
|
||||
.memberId(MEMBER_ID)
|
||||
.memberCardId(CARD_ID)
|
||||
.build();
|
||||
|
||||
when(memberCardService.useCard(RECORD_ID, MEMBER_ID, null, 50.0))
|
||||
.thenReturn(Mono.just(record));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.queryParam("recordId", String.valueOf(RECORD_ID))
|
||||
.queryParam("memberId", String.valueOf(MEMBER_ID))
|
||||
.queryParam("deductAmount", "50")
|
||||
.build();
|
||||
|
||||
Mono<ServerResponse> result = memberCardHandler.useCard(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardService).useCard(RECORD_ID, MEMBER_ID, null, 50.0);
|
||||
}
|
||||
|
||||
// ==================== refundCard ====================
|
||||
|
||||
@Test
|
||||
void refundCard_shouldReturnNoContent() {
|
||||
when(memberCardService.refundCard(RECORD_ID, MEMBER_ID))
|
||||
.thenReturn(Mono.empty());
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.queryParam("recordId", String.valueOf(RECORD_ID))
|
||||
.queryParam("memberId", String.valueOf(MEMBER_ID))
|
||||
.build();
|
||||
|
||||
Mono<ServerResponse> result = memberCardHandler.refundCard(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT);
|
||||
|
||||
verify(memberCardService).refundCard(RECORD_ID, MEMBER_ID);
|
||||
}
|
||||
|
||||
// ==================== getActiveCards ====================
|
||||
|
||||
@Test
|
||||
void getActiveCards_shouldReturnOkWithStatusParam() {
|
||||
MemberCard card1 = MemberCard.builder()
|
||||
.memberCardId(1L)
|
||||
.memberCardName("月卡")
|
||||
.memberCardType("TIME_CARD")
|
||||
.memberCardPrice(199.0)
|
||||
.memberCardStatus(1)
|
||||
.build();
|
||||
MemberCard card2 = MemberCard.builder()
|
||||
.memberCardId(2L)
|
||||
.memberCardName("季卡")
|
||||
.memberCardType("TIME_CARD")
|
||||
.memberCardPrice(499.0)
|
||||
.memberCardStatus(1)
|
||||
.build();
|
||||
|
||||
when(memberCardService.findActiveCards(1)).thenReturn(Flux.just(card1, card2));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.queryParam("status", "1")
|
||||
.build();
|
||||
|
||||
Mono<ServerResponse> result = memberCardHandler.getActiveCards(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardService).findActiveCards(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getActiveCards_shouldReturnOkWithDefaultStatus() {
|
||||
when(memberCardService.findActiveCards(1)).thenReturn(Flux.empty());
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder().build();
|
||||
|
||||
Mono<ServerResponse> result = memberCardHandler.getActiveCards(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardService).findActiveCards(1);
|
||||
}
|
||||
}
|
||||
+403
@@ -0,0 +1,403 @@
|
||||
package cn.novalon.gym.manage.member.handler;
|
||||
|
||||
import cn.novalon.gym.manage.member.entity.MemberCardRecord;
|
||||
import cn.novalon.gym.manage.member.service.IMemberCardRecordService;
|
||||
import cn.novalon.gym.manage.member.service.IMemberCardService;
|
||||
import cn.novalon.gym.manage.sys.util.AuthUtil;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class MemberCardRecordHandlerTest {
|
||||
|
||||
@Mock
|
||||
private IMemberCardService memberCardService;
|
||||
|
||||
@Mock
|
||||
private IMemberCardRecordService memberCardRecordService;
|
||||
|
||||
@Mock
|
||||
private AuthUtil authUtil;
|
||||
|
||||
private MemberCardRecordHandler handler;
|
||||
|
||||
private static final Long MEMBER_ID = 10001L;
|
||||
private static final Long RECORD_ID = 20001L;
|
||||
private static final Long MEMBER_CARD_ID = 30001L;
|
||||
private static final Long SOURCE_ORDER_ID = 40001L;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
handler = new MemberCardRecordHandler(memberCardService, memberCardRecordService, authUtil);
|
||||
}
|
||||
|
||||
private MemberCardRecord createRecord() {
|
||||
return MemberCardRecord.builder()
|
||||
.id(RECORD_ID)
|
||||
.memberId(MEMBER_ID)
|
||||
.memberCardId(MEMBER_CARD_ID)
|
||||
.remainingTimes(10)
|
||||
.remainingAmount(100.0)
|
||||
.build();
|
||||
}
|
||||
|
||||
// ==================== purchaseCard ====================
|
||||
|
||||
@Test
|
||||
void purchaseCard_shouldReturnOk() {
|
||||
MemberCardRecord record = createRecord();
|
||||
MemberCardRecordHandler.PurchaseRequest body = new MemberCardRecordHandler.PurchaseRequest();
|
||||
body.setMemberCardId(MEMBER_CARD_ID);
|
||||
body.setSourceOrderId(SOURCE_ORDER_ID);
|
||||
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID);
|
||||
when(memberCardService.purchaseCard(MEMBER_ID, MEMBER_CARD_ID, SOURCE_ORDER_ID))
|
||||
.thenReturn(Mono.just(record));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.body(Mono.just(body));
|
||||
Mono<ServerResponse> result = handler.purchaseCard(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardService).purchaseCard(MEMBER_ID, MEMBER_CARD_ID, SOURCE_ORDER_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
void purchaseCard_shouldReturn400WhenServiceThrows() {
|
||||
MemberCardRecordHandler.PurchaseRequest body = new MemberCardRecordHandler.PurchaseRequest();
|
||||
body.setMemberCardId(MEMBER_CARD_ID);
|
||||
body.setSourceOrderId(SOURCE_ORDER_ID);
|
||||
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID);
|
||||
when(memberCardService.purchaseCard(MEMBER_ID, MEMBER_CARD_ID, SOURCE_ORDER_ID))
|
||||
.thenReturn(Mono.error(new RuntimeException("购买失败")));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.body(Mono.just(body));
|
||||
Mono<ServerResponse> result = handler.purchaseCard(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
||||
|
||||
verify(memberCardService).purchaseCard(MEMBER_ID, MEMBER_CARD_ID, SOURCE_ORDER_ID);
|
||||
}
|
||||
|
||||
// ==================== renewCard ====================
|
||||
|
||||
@Test
|
||||
void renewCard_shouldReturnOk() {
|
||||
MemberCardRecord record = createRecord();
|
||||
MemberCardRecordHandler.RenewRequest body = new MemberCardRecordHandler.RenewRequest();
|
||||
body.setAddTimes(5);
|
||||
body.setAddAmount(50.0);
|
||||
body.setAddDays(30);
|
||||
body.setSourceOrderId(SOURCE_ORDER_ID);
|
||||
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID);
|
||||
when(memberCardService.renewCard(RECORD_ID, MEMBER_ID, 5, 50.0, 30, SOURCE_ORDER_ID))
|
||||
.thenReturn(Mono.just(record));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.pathVariable("recordId", RECORD_ID.toString())
|
||||
.body(Mono.just(body));
|
||||
Mono<ServerResponse> result = handler.renewCard(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardService).renewCard(RECORD_ID, MEMBER_ID, 5, 50.0, 30, SOURCE_ORDER_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
void renewCard_shouldReturn400WhenServiceThrows() {
|
||||
MemberCardRecordHandler.RenewRequest body = new MemberCardRecordHandler.RenewRequest();
|
||||
body.setAddTimes(5);
|
||||
body.setAddAmount(50.0);
|
||||
body.setAddDays(30);
|
||||
body.setSourceOrderId(SOURCE_ORDER_ID);
|
||||
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID);
|
||||
when(memberCardService.renewCard(RECORD_ID, MEMBER_ID, 5, 50.0, 30, SOURCE_ORDER_ID))
|
||||
.thenReturn(Mono.error(new RuntimeException("续费失败")));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.pathVariable("recordId", RECORD_ID.toString())
|
||||
.body(Mono.just(body));
|
||||
Mono<ServerResponse> result = handler.renewCard(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
||||
|
||||
verify(memberCardService).renewCard(RECORD_ID, MEMBER_ID, 5, 50.0, 30, SOURCE_ORDER_ID);
|
||||
}
|
||||
|
||||
// ==================== useCard ====================
|
||||
|
||||
@Test
|
||||
void useCard_shouldReturnOk() {
|
||||
MemberCardRecord record = createRecord();
|
||||
MemberCardRecordHandler.UseCardRequest body = new MemberCardRecordHandler.UseCardRequest();
|
||||
body.setDeductTimes(1);
|
||||
body.setDeductAmount(0.0);
|
||||
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID);
|
||||
when(memberCardService.useCard(RECORD_ID, MEMBER_ID, 1, 0.0))
|
||||
.thenReturn(Mono.just(record));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.pathVariable("recordId", RECORD_ID.toString())
|
||||
.body(Mono.just(body));
|
||||
Mono<ServerResponse> result = handler.useCard(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardService).useCard(RECORD_ID, MEMBER_ID, 1, 0.0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void useCard_shouldReturn400WhenServiceThrows() {
|
||||
MemberCardRecordHandler.UseCardRequest body = new MemberCardRecordHandler.UseCardRequest();
|
||||
body.setDeductTimes(1);
|
||||
body.setDeductAmount(0.0);
|
||||
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID);
|
||||
when(memberCardService.useCard(RECORD_ID, MEMBER_ID, 1, 0.0))
|
||||
.thenReturn(Mono.error(new RuntimeException("使用失败")));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.pathVariable("recordId", RECORD_ID.toString())
|
||||
.body(Mono.just(body));
|
||||
Mono<ServerResponse> result = handler.useCard(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
||||
|
||||
verify(memberCardService).useCard(RECORD_ID, MEMBER_ID, 1, 0.0);
|
||||
}
|
||||
|
||||
// ==================== refundCard ====================
|
||||
|
||||
@Test
|
||||
void refundCard_shouldReturnOk() {
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID);
|
||||
when(memberCardService.refundCard(RECORD_ID, MEMBER_ID)).thenReturn(Mono.empty());
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.pathVariable("recordId", RECORD_ID.toString())
|
||||
.build();
|
||||
Mono<ServerResponse> result = handler.refundCard(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardService).refundCard(RECORD_ID, MEMBER_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
void refundCard_shouldReturn400WhenServiceThrows() {
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID);
|
||||
when(memberCardService.refundCard(RECORD_ID, MEMBER_ID))
|
||||
.thenReturn(Mono.error(new RuntimeException("退款失败")));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.pathVariable("recordId", RECORD_ID.toString())
|
||||
.build();
|
||||
Mono<ServerResponse> result = handler.refundCard(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
||||
|
||||
verify(memberCardService).refundCard(RECORD_ID, MEMBER_ID);
|
||||
}
|
||||
|
||||
// ==================== getMemberCardRecordById ====================
|
||||
|
||||
@Test
|
||||
void getMemberCardRecordById_shouldReturnOk() {
|
||||
MemberCardRecord record = createRecord();
|
||||
|
||||
when(memberCardRecordService.findById(RECORD_ID)).thenReturn(Mono.just(record));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.pathVariable("recordId", RECORD_ID.toString())
|
||||
.build();
|
||||
Mono<ServerResponse> result = handler.getMemberCardRecordById(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardRecordService).findById(RECORD_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getMemberCardRecordById_shouldReturn404WhenNotFound() {
|
||||
when(memberCardRecordService.findById(RECORD_ID)).thenReturn(Mono.empty());
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.pathVariable("recordId", RECORD_ID.toString())
|
||||
.build();
|
||||
Mono<ServerResponse> result = handler.getMemberCardRecordById(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
||||
|
||||
verify(memberCardRecordService).findById(RECORD_ID);
|
||||
}
|
||||
|
||||
// ==================== getMyCards ====================
|
||||
|
||||
@Test
|
||||
void getMyCards_shouldReturnOk() {
|
||||
MemberCardRecord record = createRecord();
|
||||
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID);
|
||||
when(memberCardRecordService.findActiveCardsByMemberId(MEMBER_ID))
|
||||
.thenReturn(Flux.just(record));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder().build();
|
||||
Mono<ServerResponse> result = handler.getMyCards(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardRecordService).findActiveCardsByMemberId(MEMBER_ID);
|
||||
}
|
||||
|
||||
// ==================== getMyCardsWithStatus ====================
|
||||
|
||||
@Test
|
||||
void getMyCardsWithStatus_shouldReturnOkWithAll() {
|
||||
MemberCardRecord record = createRecord();
|
||||
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID);
|
||||
when(memberCardRecordService.findCardsByMemberIdWithStatus(MEMBER_ID, "all"))
|
||||
.thenReturn(Flux.just(record));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.queryParam("status", "all")
|
||||
.build();
|
||||
Mono<ServerResponse> result = handler.getMyCardsWithStatus(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardRecordService).findCardsByMemberIdWithStatus(MEMBER_ID, "all");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getMyCardsWithStatus_shouldReturnOkWithActive() {
|
||||
MemberCardRecord record = createRecord();
|
||||
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID);
|
||||
when(memberCardRecordService.findCardsByMemberIdWithStatus(MEMBER_ID, "active"))
|
||||
.thenReturn(Flux.just(record));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.queryParam("status", "active")
|
||||
.build();
|
||||
Mono<ServerResponse> result = handler.getMyCardsWithStatus(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardRecordService).findCardsByMemberIdWithStatus(MEMBER_ID, "active");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getMyCardsWithStatus_shouldReturnOkWithExpired() {
|
||||
MemberCardRecord record = createRecord();
|
||||
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID);
|
||||
when(memberCardRecordService.findCardsByMemberIdWithStatus(MEMBER_ID, "expired"))
|
||||
.thenReturn(Flux.just(record));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.queryParam("status", "expired")
|
||||
.build();
|
||||
Mono<ServerResponse> result = handler.getMyCardsWithStatus(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardRecordService).findCardsByMemberIdWithStatus(MEMBER_ID, "expired");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getMyCardsWithStatus_shouldReturn400WhenInvalidStatus() {
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID);
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.queryParam("status", "invalid")
|
||||
.build();
|
||||
Mono<ServerResponse> result = handler.getMyCardsWithStatus(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
||||
|
||||
verify(memberCardRecordService, never()).findCardsByMemberIdWithStatus(anyLong(), anyString());
|
||||
}
|
||||
|
||||
// ==================== getPrimaryCard ====================
|
||||
|
||||
@Test
|
||||
void getPrimaryCard_shouldReturnOk() {
|
||||
MemberCardRecord record = createRecord();
|
||||
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID);
|
||||
when(memberCardRecordService.findPrimaryActiveCardByMemberId(MEMBER_ID))
|
||||
.thenReturn(Mono.just(record));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder().build();
|
||||
Mono<ServerResponse> result = handler.getPrimaryCard(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardRecordService).findPrimaryActiveCardByMemberId(MEMBER_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPrimaryCard_shouldReturn204WhenNoCard() {
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID);
|
||||
when(memberCardRecordService.findPrimaryActiveCardByMemberId(MEMBER_ID))
|
||||
.thenReturn(Mono.empty());
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder().build();
|
||||
Mono<ServerResponse> result = handler.getPrimaryCard(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT);
|
||||
|
||||
verify(memberCardRecordService).findPrimaryActiveCardByMemberId(MEMBER_ID);
|
||||
}
|
||||
|
||||
// ==================== processExpiredCards ====================
|
||||
|
||||
@Test
|
||||
void processExpiredCards_shouldReturnOk() {
|
||||
when(memberCardService.processExpiredCards()).thenReturn(Mono.just(5));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder().build();
|
||||
Mono<ServerResponse> result = handler.processExpiredCards(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardService).processExpiredCards();
|
||||
}
|
||||
}
|
||||
+316
@@ -0,0 +1,316 @@
|
||||
package cn.novalon.gym.manage.member.handler;
|
||||
|
||||
import cn.hutool.db.PageResult;
|
||||
import cn.novalon.gym.manage.member.entity.MemberCardTransaction;
|
||||
import cn.novalon.gym.manage.member.enums.TransactionType;
|
||||
import cn.novalon.gym.manage.member.service.IMemberCardTransactionService;
|
||||
import jakarta.validation.Validator;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class MemberCardTransactionHandlerTest {
|
||||
|
||||
@Mock
|
||||
private IMemberCardTransactionService memberCardTransactionService;
|
||||
|
||||
@Mock(lenient = true)
|
||||
private Validator validator;
|
||||
|
||||
private MemberCardTransactionHandler handler;
|
||||
|
||||
private static final Long MEMBER_ID = 10001L;
|
||||
private static final Long MEMBER_CARD_ID = 20001L;
|
||||
private static final Long RECORD_ID = 30001L;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
handler = new MemberCardTransactionHandler(memberCardTransactionService, validator);
|
||||
}
|
||||
|
||||
// ==================== insertTransaction ====================
|
||||
|
||||
@Test
|
||||
void insertTransaction_shouldReturnOk() {
|
||||
MemberCardTransaction transaction = MemberCardTransaction.builder()
|
||||
.id(1L)
|
||||
.memberId(MEMBER_ID)
|
||||
.memberCardId(MEMBER_CARD_ID)
|
||||
.operationType(TransactionType.PURCHASE.name())
|
||||
.changeAmount(1)
|
||||
.changeBalance(1000.0)
|
||||
.build();
|
||||
|
||||
when(memberCardTransactionService.insertTransaction(any(MemberCardTransaction.class)))
|
||||
.thenReturn(Mono.just(transaction));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.body(Mono.just(transaction));
|
||||
|
||||
Mono<ServerResponse> result = handler.insertTransaction(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardTransactionService).insertTransaction(any(MemberCardTransaction.class));
|
||||
}
|
||||
|
||||
// ==================== getMemberTransactions ====================
|
||||
|
||||
@Test
|
||||
void getMemberTransactions_shouldReturnOkWithDefaultTimeRange() {
|
||||
MemberCardTransaction t1 = MemberCardTransaction.builder()
|
||||
.id(1L).memberId(MEMBER_ID).memberCardId(MEMBER_CARD_ID)
|
||||
.operationType(TransactionType.DEDUCT.name())
|
||||
.build();
|
||||
MemberCardTransaction t2 = MemberCardTransaction.builder()
|
||||
.id(2L).memberId(MEMBER_ID).memberCardId(MEMBER_CARD_ID)
|
||||
.operationType(TransactionType.DEDUCT.name())
|
||||
.build();
|
||||
|
||||
when(memberCardTransactionService.findByMemberIdAndTimeRange(
|
||||
eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class), any()))
|
||||
.thenReturn(Flux.just(t1, t2));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.pathVariable("memberId", String.valueOf(MEMBER_ID))
|
||||
.build();
|
||||
|
||||
Mono<ServerResponse> result = handler.getMemberTransactions(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardTransactionService).findByMemberIdAndTimeRange(
|
||||
eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getMemberTransactions_shouldReturnOkWithCustomTimeRange() {
|
||||
MemberCardTransaction t1 = MemberCardTransaction.builder()
|
||||
.id(1L).memberId(MEMBER_ID)
|
||||
.build();
|
||||
|
||||
when(memberCardTransactionService.findByMemberIdAndTimeRange(
|
||||
eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class), any()))
|
||||
.thenReturn(Flux.just(t1));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.pathVariable("memberId", String.valueOf(MEMBER_ID))
|
||||
.queryParam("startTime", "2026-01-01T00:00:00")
|
||||
.queryParam("endTime", "2026-06-30T23:59:59")
|
||||
.queryParam("page", "0")
|
||||
.queryParam("size", "5")
|
||||
.build();
|
||||
|
||||
Mono<ServerResponse> result = handler.getMemberTransactions(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardTransactionService).findByMemberIdAndTimeRange(
|
||||
eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class), any());
|
||||
}
|
||||
|
||||
// ==================== getTransactionsWithConditions ====================
|
||||
|
||||
@Test
|
||||
void getTransactionsWithConditions_shouldReturnOkWithPageResult() {
|
||||
MemberCardTransaction t1 = MemberCardTransaction.builder()
|
||||
.id(1L).memberId(MEMBER_ID)
|
||||
.operationType(TransactionType.PURCHASE.name())
|
||||
.build();
|
||||
MemberCardTransaction t2 = MemberCardTransaction.builder()
|
||||
.id(2L).memberId(MEMBER_ID)
|
||||
.operationType(TransactionType.PURCHASE.name())
|
||||
.build();
|
||||
List<MemberCardTransaction> list = List.of(t1, t2);
|
||||
|
||||
when(memberCardTransactionService.countWithConditions(
|
||||
any(), any(), any(), any(), any()))
|
||||
.thenReturn(Mono.just(2L));
|
||||
when(memberCardTransactionService.findWithConditions(
|
||||
any(), any(), any(), any(), any(), any()))
|
||||
.thenReturn(Flux.fromIterable(list));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.queryParam("memberId", String.valueOf(MEMBER_ID))
|
||||
.queryParam("operationType", "PURCHASE")
|
||||
.queryParam("page", "0")
|
||||
.queryParam("size", "10")
|
||||
.build();
|
||||
|
||||
Mono<ServerResponse> result = handler.getTransactionsWithConditions(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardTransactionService).countWithConditions(
|
||||
any(), any(), any(), any(), any());
|
||||
verify(memberCardTransactionService).findWithConditions(
|
||||
any(), any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
// ==================== getTransactionsByCardId ====================
|
||||
|
||||
@Test
|
||||
void getTransactionsByCardId_shouldReturnOk() {
|
||||
MemberCardTransaction t1 = MemberCardTransaction.builder()
|
||||
.id(1L).memberCardId(MEMBER_CARD_ID)
|
||||
.build();
|
||||
|
||||
when(memberCardTransactionService.findByMemberCardId(MEMBER_CARD_ID))
|
||||
.thenReturn(Flux.just(t1));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.pathVariable("cardId", String.valueOf(MEMBER_CARD_ID))
|
||||
.build();
|
||||
|
||||
Mono<ServerResponse> result = handler.getTransactionsByCardId(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardTransactionService).findByMemberCardId(MEMBER_CARD_ID);
|
||||
}
|
||||
|
||||
// ==================== getTransactionsByRecordId ====================
|
||||
|
||||
@Test
|
||||
void getTransactionsByRecordId_shouldReturnOk() {
|
||||
MemberCardTransaction t1 = MemberCardTransaction.builder()
|
||||
.id(1L).memberCardRecordId(RECORD_ID)
|
||||
.build();
|
||||
|
||||
when(memberCardTransactionService.findByRecordId(RECORD_ID))
|
||||
.thenReturn(Flux.just(t1));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.pathVariable("recordId", String.valueOf(RECORD_ID))
|
||||
.build();
|
||||
|
||||
Mono<ServerResponse> result = handler.getTransactionsByRecordId(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardTransactionService).findByRecordId(RECORD_ID);
|
||||
}
|
||||
|
||||
// ==================== getDeductCountByCardId ====================
|
||||
|
||||
@Test
|
||||
void getDeductCountByCardId_shouldReturnOk() {
|
||||
when(memberCardTransactionService.sumDeductCountByCardId(
|
||||
eq(MEMBER_CARD_ID), any(LocalDateTime.class), any(LocalDateTime.class)))
|
||||
.thenReturn(Mono.just(5L));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.pathVariable("cardId", String.valueOf(MEMBER_CARD_ID))
|
||||
.build();
|
||||
|
||||
Mono<ServerResponse> result = handler.getDeductCountByCardId(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardTransactionService).sumDeductCountByCardId(
|
||||
eq(MEMBER_CARD_ID), any(LocalDateTime.class), any(LocalDateTime.class));
|
||||
}
|
||||
|
||||
// ==================== getRenewAmountByTimeRange ====================
|
||||
|
||||
@Test
|
||||
void getRenewAmountByTimeRange_shouldReturnOk() {
|
||||
when(memberCardTransactionService.sumRenewAmountByTimeRange(
|
||||
any(LocalDateTime.class), any(LocalDateTime.class)))
|
||||
.thenReturn(Mono.just(5000.0));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.queryParam("startTime", "2026-01-01T00:00:00")
|
||||
.queryParam("endTime", "2026-06-30T23:59:59")
|
||||
.build();
|
||||
|
||||
Mono<ServerResponse> result = handler.getRenewAmountByTimeRange(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardTransactionService).sumRenewAmountByTimeRange(
|
||||
any(LocalDateTime.class), any(LocalDateTime.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRenewAmountByTimeRange_shouldReturnOkWithDefaultTimeRange() {
|
||||
when(memberCardTransactionService.sumRenewAmountByTimeRange(
|
||||
any(LocalDateTime.class), any(LocalDateTime.class)))
|
||||
.thenReturn(Mono.just(0.0));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder().build();
|
||||
|
||||
Mono<ServerResponse> result = handler.getRenewAmountByTimeRange(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardTransactionService).sumRenewAmountByTimeRange(
|
||||
any(LocalDateTime.class), any(LocalDateTime.class));
|
||||
}
|
||||
|
||||
// ==================== getPurchaseAmountByMember ====================
|
||||
|
||||
@Test
|
||||
void getPurchaseAmountByMember_shouldReturnOk() {
|
||||
when(memberCardTransactionService.sumPurchaseAmountByMemberId(
|
||||
eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class)))
|
||||
.thenReturn(Mono.just(3000.0));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.pathVariable("memberId", String.valueOf(MEMBER_ID))
|
||||
.build();
|
||||
|
||||
Mono<ServerResponse> result = handler.getPurchaseAmountByMember(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardTransactionService).sumPurchaseAmountByMemberId(
|
||||
eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPurchaseAmountByMember_shouldReturnOkWithCustomTimeRange() {
|
||||
when(memberCardTransactionService.sumPurchaseAmountByMemberId(
|
||||
eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class)))
|
||||
.thenReturn(Mono.just(0.0));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.pathVariable("memberId", String.valueOf(MEMBER_ID))
|
||||
.queryParam("startTime", "2026-01-01T00:00:00")
|
||||
.queryParam("endTime", "2026-06-30T23:59:59")
|
||||
.build();
|
||||
|
||||
Mono<ServerResponse> result = handler.getPurchaseAmountByMember(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberCardTransactionService).sumPurchaseAmountByMemberId(
|
||||
eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class));
|
||||
}
|
||||
}
|
||||
+388
@@ -0,0 +1,388 @@
|
||||
package cn.novalon.gym.manage.member.handler;
|
||||
|
||||
import cn.novalon.gym.manage.common.exception.NotFoundException;
|
||||
import cn.novalon.gym.manage.member.dto.AdminUpdatePhoneDto;
|
||||
import cn.novalon.gym.manage.member.dto.SearchMemberDto;
|
||||
import cn.novalon.gym.manage.member.dto.UpdateMemberInfoDto;
|
||||
import cn.novalon.gym.manage.member.entity.Member;
|
||||
import cn.novalon.gym.manage.member.es.entity.MemberES;
|
||||
import cn.novalon.gym.manage.member.service.MemberService;
|
||||
import cn.novalon.gym.manage.member.service.WechatAuthService;
|
||||
import cn.novalon.gym.manage.member.service.WechatOfficialService;
|
||||
import cn.novalon.gym.manage.member.vo.MemberDetailVO;
|
||||
import cn.novalon.gym.manage.member.vo.MemberInfoVO;
|
||||
import cn.novalon.gym.manage.sys.util.AuthUtil;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class MemberHandlerTest {
|
||||
|
||||
@Mock
|
||||
private MemberService memberService;
|
||||
|
||||
@Mock
|
||||
private WechatAuthService wechatAuthService;
|
||||
|
||||
@Mock
|
||||
private WechatOfficialService wechatOfficialService;
|
||||
|
||||
@Mock
|
||||
private AuthUtil authUtil;
|
||||
|
||||
private MemberHandler memberHandler;
|
||||
|
||||
private static final Long MEMBER_ID = 10001L;
|
||||
private static final Long ADMIN_ID = 1L;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
memberHandler = new MemberHandler(memberService, wechatAuthService, wechatOfficialService, authUtil);
|
||||
}
|
||||
|
||||
// ==================== getMemberInfo ====================
|
||||
|
||||
@Test
|
||||
void getMemberInfo_shouldReturnOk() {
|
||||
MemberInfoVO info = MemberInfoVO.builder().id(MEMBER_ID).nickname("test").build();
|
||||
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID);
|
||||
when(memberService.getMemberInfo(MEMBER_ID)).thenReturn(Mono.just(info));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder().build();
|
||||
Mono<ServerResponse> result = memberHandler.getMemberInfo(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberService).getMemberInfo(MEMBER_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getMemberInfo_shouldReturn404WhenNotFound() {
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID);
|
||||
when(memberService.getMemberInfo(MEMBER_ID))
|
||||
.thenReturn(Mono.error(new NotFoundException("MEMBER_NOT_FOUND", "会员不存在")));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder().build();
|
||||
Mono<ServerResponse> result = memberHandler.getMemberInfo(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
// ==================== updateMemberInfo ====================
|
||||
|
||||
@Test
|
||||
void updateMemberInfo_shouldReturnOk() {
|
||||
UpdateMemberInfoDto updateDto = new UpdateMemberInfoDto();
|
||||
updateDto.setNickname("newName");
|
||||
MemberInfoVO updatedInfo = MemberInfoVO.builder().id(MEMBER_ID).nickname("newName").build();
|
||||
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID);
|
||||
when(memberService.updateMemberInfo(eq(MEMBER_ID), any(UpdateMemberInfoDto.class)))
|
||||
.thenReturn(Mono.just(updatedInfo));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.body(Mono.just(updateDto));
|
||||
|
||||
Mono<ServerResponse> result = memberHandler.updateMemberInfo(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberService).updateMemberInfo(eq(MEMBER_ID), any(UpdateMemberInfoDto.class));
|
||||
}
|
||||
|
||||
// ==================== bindPhone ====================
|
||||
|
||||
@Test
|
||||
void bindPhone_shouldReturnOk() {
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID);
|
||||
when(wechatAuthService.bindPhone(MEMBER_ID, "phoneCode123")).thenReturn(Mono.just(true));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.queryParam("phoneCode", "phoneCode123")
|
||||
.build();
|
||||
|
||||
Mono<ServerResponse> result = memberHandler.bindPhone(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(wechatAuthService).bindPhone(MEMBER_ID, "phoneCode123");
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindPhone_shouldThrowWhenPhoneCodeEmpty() {
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID);
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder().build();
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> memberHandler.bindPhone(request));
|
||||
}
|
||||
|
||||
// ==================== checkSubscribeStatus ====================
|
||||
|
||||
@Test
|
||||
void checkSubscribeStatus_shouldReturnOkWhenSubscribed() {
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID);
|
||||
when(wechatOfficialService.checkSubscribeStatus(MEMBER_ID)).thenReturn(Mono.just(true));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder().build();
|
||||
|
||||
Mono<ServerResponse> result = memberHandler.checkSubscribeStatus(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(wechatOfficialService).checkSubscribeStatus(MEMBER_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkSubscribeStatus_shouldReturnOkWhenNotSubscribed() {
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID);
|
||||
when(wechatOfficialService.checkSubscribeStatus(MEMBER_ID)).thenReturn(Mono.just(false));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder().build();
|
||||
|
||||
Mono<ServerResponse> result = memberHandler.checkSubscribeStatus(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(wechatOfficialService).checkSubscribeStatus(MEMBER_ID);
|
||||
}
|
||||
|
||||
// ==================== adminUpdatePhone ====================
|
||||
|
||||
@Test
|
||||
void adminUpdatePhone_shouldReturnOk() {
|
||||
AdminUpdatePhoneDto body = new AdminUpdatePhoneDto();
|
||||
body.setPhone("13800138000");
|
||||
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(ADMIN_ID);
|
||||
when(memberService.adminUpdatePhone(1L, "13800138000")).thenReturn(Mono.just(true));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.pathVariable("id", "1")
|
||||
.body(Mono.just(body));
|
||||
|
||||
Mono<ServerResponse> result = memberHandler.adminUpdatePhone(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberService).adminUpdatePhone(1L, "13800138000");
|
||||
}
|
||||
|
||||
@Test
|
||||
void adminUpdatePhone_shouldThrowWhenMemberIdInvalid() {
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(ADMIN_ID);
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.pathVariable("id", "0")
|
||||
.build();
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> memberHandler.adminUpdatePhone(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
void adminUpdatePhone_shouldThrowWhenPhoneEmpty() {
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(ADMIN_ID);
|
||||
|
||||
AdminUpdatePhoneDto body = new AdminUpdatePhoneDto();
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.pathVariable("id", "1")
|
||||
.body(Mono.just(body));
|
||||
|
||||
Mono<ServerResponse> result = memberHandler.adminUpdatePhone(request);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> result.block());
|
||||
}
|
||||
|
||||
@Test
|
||||
void adminUpdatePhone_shouldThrowWhenPhoneFormatInvalid() {
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(ADMIN_ID);
|
||||
|
||||
AdminUpdatePhoneDto body = new AdminUpdatePhoneDto();
|
||||
body.setPhone("12345");
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.pathVariable("id", "1")
|
||||
.body(Mono.just(body));
|
||||
|
||||
Mono<ServerResponse> result = memberHandler.adminUpdatePhone(request);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> result.block());
|
||||
}
|
||||
|
||||
// ==================== adminGetMemberInfo ====================
|
||||
|
||||
@Test
|
||||
void adminGetMemberInfo_shouldReturnOk() {
|
||||
MemberDetailVO detail = MemberDetailVO.builder().id(1L).nickname("test").build();
|
||||
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(ADMIN_ID);
|
||||
when(memberService.getMemberDetail(1L)).thenReturn(Mono.just(detail));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.pathVariable("id", "1")
|
||||
.build();
|
||||
|
||||
Mono<ServerResponse> result = memberHandler.adminGetMemberInfo(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberService).getMemberDetail(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void adminGetMemberInfo_shouldThrowWhenMemberIdInvalid() {
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(ADMIN_ID);
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.pathVariable("id", "0")
|
||||
.build();
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> memberHandler.adminGetMemberInfo(request));
|
||||
}
|
||||
|
||||
// ==================== adminUpdateMemberInfo ====================
|
||||
|
||||
@Test
|
||||
void adminUpdateMemberInfo_shouldReturnOk() {
|
||||
UpdateMemberInfoDto updateDto = new UpdateMemberInfoDto();
|
||||
updateDto.setNickname("adminUpdate");
|
||||
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(ADMIN_ID);
|
||||
when(memberService.adminUpdateMemberInfo(eq(1L), any(UpdateMemberInfoDto.class)))
|
||||
.thenReturn(Mono.just(true));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.pathVariable("id", "1")
|
||||
.body(Mono.just(updateDto));
|
||||
|
||||
Mono<ServerResponse> result = memberHandler.adminUpdateMemberInfo(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberService).adminUpdateMemberInfo(eq(1L), any(UpdateMemberInfoDto.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void adminUpdateMemberInfo_shouldThrowWhenMemberIdInvalid() {
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(ADMIN_ID);
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.pathVariable("id", "0")
|
||||
.build();
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> memberHandler.adminUpdateMemberInfo(request));
|
||||
}
|
||||
|
||||
// ==================== searchMembers ====================
|
||||
|
||||
@Test
|
||||
void searchMembers_shouldReturnOkWithResults() {
|
||||
MemberES member1 = new MemberES("1", "M001", "test1", "138****8000", 1, "avatar1");
|
||||
MemberES member2 = new MemberES("2", "M002", "test2", "139****8001", 2, "avatar2");
|
||||
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(ADMIN_ID);
|
||||
when(memberService.searchMember(any(SearchMemberDto.class)))
|
||||
.thenReturn(Flux.just(member1, member2));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.queryParam("searchValue", "test")
|
||||
.queryParam("pageNum", "1")
|
||||
.queryParam("pageSize", "10")
|
||||
.build();
|
||||
|
||||
Mono<ServerResponse> result = memberHandler.searchMembers(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberService).searchMember(any(SearchMemberDto.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void searchMembers_shouldReturnOkWithEmptyResults() {
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(ADMIN_ID);
|
||||
when(memberService.searchMember(any(SearchMemberDto.class)))
|
||||
.thenReturn(Flux.empty());
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.queryParam("searchValue", "nonexistent")
|
||||
.build();
|
||||
|
||||
Mono<ServerResponse> result = memberHandler.searchMembers(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberService).searchMember(any(SearchMemberDto.class));
|
||||
}
|
||||
|
||||
// ==================== getAllMembers ====================
|
||||
|
||||
@Test
|
||||
void getAllMembers_shouldReturnOkWithResults() {
|
||||
Member member1 = new Member();
|
||||
member1.setId(1L);
|
||||
member1.setMemberNo("M001");
|
||||
member1.setNickname("test1");
|
||||
Member member2 = new Member();
|
||||
member2.setId(2L);
|
||||
member2.setMemberNo("M002");
|
||||
member2.setNickname("test2");
|
||||
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(ADMIN_ID);
|
||||
when(memberService.findAll(anyInt(), anyInt(), any(), any()))
|
||||
.thenReturn(Flux.just(member1, member2));
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.queryParam("pageNum", "1")
|
||||
.queryParam("pageSize", "10")
|
||||
.build();
|
||||
|
||||
Mono<ServerResponse> result = memberHandler.getAllMembers(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberService).findAll(anyInt(), anyInt(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAllMembers_shouldReturnOkWithEmptyResults() {
|
||||
when(authUtil.getMemberIdOrThrow(any())).thenReturn(ADMIN_ID);
|
||||
when(memberService.findAll(anyInt(), anyInt(), any(), any()))
|
||||
.thenReturn(Flux.empty());
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder().build();
|
||||
|
||||
Mono<ServerResponse> result = memberHandler.getAllMembers(request);
|
||||
|
||||
ServerResponse response = result.block();
|
||||
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
verify(memberService).findAll(anyInt(), anyInt(), any(), any());
|
||||
}
|
||||
}
|
||||
+460
@@ -0,0 +1,460 @@
|
||||
package cn.novalon.gym.manage.member.service.impl;
|
||||
|
||||
import cn.novalon.gym.manage.common.util.RedisUtil;
|
||||
import cn.novalon.gym.manage.member.entity.MemberCard;
|
||||
import cn.novalon.gym.manage.member.entity.MemberCardRecord;
|
||||
import cn.novalon.gym.manage.member.enums.MemberCardRecordStatus;
|
||||
import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository;
|
||||
import cn.novalon.gym.manage.member.repository.MemberCardRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
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 org.springframework.data.domain.Pageable;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@DisplayName("MemberCardRecordServiceImpl 单元测试")
|
||||
class MemberCardRecordServiceImplTest {
|
||||
|
||||
@Mock
|
||||
private MemberCardRecordRepository memberCardRecordRepository;
|
||||
|
||||
@Mock
|
||||
private MemberCardRepository memberCardRepository;
|
||||
|
||||
@Mock
|
||||
private RedisUtil redisUtil;
|
||||
|
||||
@InjectMocks
|
||||
private MemberCardRecordServiceImpl memberCardRecordService;
|
||||
|
||||
private static final Long RECORD_ID = 10L;
|
||||
private static final Long MEMBER_ID = 100L;
|
||||
private static final Long MEMBER_CARD_ID = 1L;
|
||||
|
||||
private MemberCardRecord createRecord() {
|
||||
MemberCardRecord record = new MemberCardRecord();
|
||||
record.setId(RECORD_ID);
|
||||
record.setMemberCardRecordId(RECORD_ID);
|
||||
record.setMemberId(MEMBER_ID);
|
||||
record.setMemberCardId(MEMBER_CARD_ID);
|
||||
record.setStatus(MemberCardRecordStatus.ACTIVE);
|
||||
record.setRemainingTimes(10);
|
||||
record.setRemainingAmount(500.0);
|
||||
record.setExpireTime(LocalDateTime.now().plusDays(30));
|
||||
return record;
|
||||
}
|
||||
|
||||
private MemberCard createCard() {
|
||||
MemberCard card = MemberCard.builder()
|
||||
.memberCardId(MEMBER_CARD_ID)
|
||||
.memberCardName("月卡")
|
||||
.memberCardType("TIME_CARD")
|
||||
.memberCardPrice(199.0)
|
||||
.memberCardValidityDays(30)
|
||||
.memberCardStatus(1)
|
||||
.build();
|
||||
card.setId(MEMBER_CARD_ID);
|
||||
return card;
|
||||
}
|
||||
|
||||
// ==================== findById ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("findById 方法")
|
||||
class FindByIdTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("缓存命中时应从缓存返回")
|
||||
void shouldReturnFromCacheWhenHit() {
|
||||
MemberCardRecord record = createRecord();
|
||||
when(redisUtil.get("member:card:record:" + RECORD_ID)).thenReturn(Mono.just(record));
|
||||
|
||||
Mono<MemberCardRecord> result = memberCardRecordService.findById(RECORD_ID);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(r -> assertThat(r.getMemberCardRecordId()).isEqualTo(RECORD_ID))
|
||||
.verifyComplete();
|
||||
|
||||
verify(memberCardRecordRepository, never()).findById(anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("缓存未命中时应从数据库查询并更新缓存")
|
||||
void shouldQueryDatabaseWhenCacheMiss() {
|
||||
MemberCardRecord record = createRecord();
|
||||
when(redisUtil.get("member:card:record:" + RECORD_ID)).thenReturn(Mono.empty());
|
||||
when(memberCardRecordRepository.findById(anyLong())).thenReturn(Mono.just(record));
|
||||
|
||||
Mono<MemberCardRecord> result = memberCardRecordService.findById(RECORD_ID);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(r -> assertThat(r.getMemberCardRecordId()).isEqualTo(RECORD_ID))
|
||||
.verifyComplete();
|
||||
|
||||
verify(memberCardRecordRepository).findById(anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("记录不存在时应返回空")
|
||||
void shouldReturnEmptyWhenNotFound() {
|
||||
when(redisUtil.get("member:card:record:" + RECORD_ID)).thenReturn(Mono.empty());
|
||||
when(memberCardRecordRepository.findById(anyLong())).thenReturn(Mono.empty());
|
||||
|
||||
Mono<MemberCardRecord> result = memberCardRecordService.findById(RECORD_ID);
|
||||
|
||||
StepVerifier.create(result).verifyComplete();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== findByMemberId ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("findByMemberId 方法")
|
||||
class FindByMemberIdTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应委托给 repository")
|
||||
void shouldDelegateToRepository() {
|
||||
MemberCardRecord record = createRecord();
|
||||
Pageable pageable = Pageable.ofSize(10);
|
||||
when(memberCardRecordRepository.findByMemberId(MEMBER_ID, pageable)).thenReturn(Flux.just(record));
|
||||
|
||||
Flux<MemberCardRecord> result = memberCardRecordService.findByMemberId(MEMBER_ID, pageable);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(r -> assertThat(r.getMemberCardRecordId()).isEqualTo(RECORD_ID))
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== findActiveCardsByMemberId ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("findActiveCardsByMemberId 方法")
|
||||
class FindActiveCardsByMemberIdTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("无记录时应返回空")
|
||||
void shouldReturnEmptyWhenNoRecords() {
|
||||
when(memberCardRecordRepository.findActiveCardsByMemberId(MEMBER_ID)).thenReturn(Flux.empty());
|
||||
|
||||
Flux<MemberCardRecord> result = memberCardRecordService.findActiveCardsByMemberId(MEMBER_ID);
|
||||
|
||||
StepVerifier.create(result).verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("有记录时应填充会员卡信息")
|
||||
void shouldEnrichCardInfoWhenRecordsExist() {
|
||||
MemberCardRecord record = createRecord();
|
||||
MemberCard card = createCard();
|
||||
when(memberCardRecordRepository.findActiveCardsByMemberId(MEMBER_ID)).thenReturn(Flux.just(record));
|
||||
when(memberCardRepository.findByIdAndDeletedAtIsNull(MEMBER_CARD_ID)).thenReturn(Mono.just(card));
|
||||
|
||||
Flux<MemberCardRecord> result = memberCardRecordService.findActiveCardsByMemberId(MEMBER_ID);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(r -> {
|
||||
assertThat(r.getMemberCardName()).isEqualTo("月卡");
|
||||
assertThat(r.getMemberCardType()).isEqualTo("TIME_CARD");
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== insertActiveRecord ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("insertActiveRecord 方法")
|
||||
class InsertActiveRecordTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应委托给 repository")
|
||||
void shouldDelegateToRepository() {
|
||||
MemberCardRecord record = createRecord();
|
||||
when(memberCardRecordRepository.insertActiveRecord(
|
||||
record.getMemberId(), record.getMemberCardId(), record.getExpireTime(),
|
||||
record.getRemainingTimes(), record.getRemainingAmount(), record.getSourceOrderId()))
|
||||
.thenReturn(Mono.just(record));
|
||||
|
||||
Mono<MemberCardRecord> result = memberCardRecordService.insertActiveRecord(record);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(r -> assertThat(r.getMemberCardRecordId()).isEqualTo(RECORD_ID))
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== deductUsage ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("deductUsage 方法")
|
||||
class DeductUsageTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("扣减成功时应清除缓存")
|
||||
void shouldClearCacheWhenDeducted() {
|
||||
when(memberCardRecordRepository.deductUsage(RECORD_ID, 1, 0.0)).thenReturn(Mono.just(1));
|
||||
when(redisUtil.delete("member:card:record:" + RECORD_ID)).thenReturn(Mono.just(1L));
|
||||
|
||||
Mono<Integer> result = memberCardRecordService.deductUsage(RECORD_ID, 1, 0.0);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(updated -> assertThat(updated).isEqualTo(1))
|
||||
.verifyComplete();
|
||||
|
||||
verify(redisUtil).delete("member:card:record:" + RECORD_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("扣减失败时不清理缓存")
|
||||
void shouldNotClearCacheWhenNotUpdated() {
|
||||
when(memberCardRecordRepository.deductUsage(RECORD_ID, 1, 0.0)).thenReturn(Mono.just(0));
|
||||
|
||||
Mono<Integer> result = memberCardRecordService.deductUsage(RECORD_ID, 1, 0.0);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(updated -> assertThat(updated).isZero())
|
||||
.verifyComplete();
|
||||
|
||||
verify(redisUtil, never()).delete(anyString());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== renewCard ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("renewCard 方法")
|
||||
class RenewCardTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("续费成功时应清除缓存")
|
||||
void shouldClearCacheWhenRenewed() {
|
||||
LocalDateTime newExpire = LocalDateTime.now().plusDays(30);
|
||||
when(memberCardRecordRepository.renewCard(RECORD_ID, 10, null, newExpire)).thenReturn(Mono.just(1));
|
||||
when(redisUtil.delete("member:card:record:" + RECORD_ID)).thenReturn(Mono.just(1L));
|
||||
|
||||
Mono<Integer> result = memberCardRecordService.renewCard(RECORD_ID, 10, null, newExpire);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(updated -> assertThat(updated).isEqualTo(1))
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== updateStatus ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("updateStatus 方法")
|
||||
class UpdateStatusTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("状态更新成功时应清除缓存")
|
||||
void shouldClearCacheWhenStatusUpdated() {
|
||||
when(memberCardRecordRepository.updateStatus(RECORD_ID, "USED_UP")).thenReturn(Mono.just(1));
|
||||
when(redisUtil.delete("member:card:record:" + RECORD_ID)).thenReturn(Mono.just(1L));
|
||||
|
||||
Mono<Integer> result = memberCardRecordService.updateStatus(RECORD_ID, "USED_UP");
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(updated -> assertThat(updated).isEqualTo(1))
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== findExpiredCards ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("findExpiredCards 方法")
|
||||
class FindExpiredCardsTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应委托给 repository")
|
||||
void shouldDelegateToRepository() {
|
||||
MemberCardRecord record = createRecord();
|
||||
when(memberCardRecordRepository.findExpiredCards()).thenReturn(Flux.just(record));
|
||||
|
||||
Flux<MemberCardRecord> result = memberCardRecordService.findExpiredCards();
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(r -> assertThat(r.getMemberCardRecordId()).isEqualTo(RECORD_ID))
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== findCardsByMemberIdWithStatus ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("findCardsByMemberIdWithStatus 方法")
|
||||
class FindCardsByMemberIdWithStatusTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("无记录时应返回空")
|
||||
void shouldReturnEmptyWhenNoRecords() {
|
||||
when(memberCardRecordRepository.findCardsByMemberIdWithStatus(MEMBER_ID, "active"))
|
||||
.thenReturn(Flux.empty());
|
||||
|
||||
Flux<MemberCardRecord> result = memberCardRecordService.findCardsByMemberIdWithStatus(MEMBER_ID, "active");
|
||||
|
||||
StepVerifier.create(result).verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("有记录时应填充会员卡信息")
|
||||
void shouldEnrichCardInfoWhenRecordsExist() {
|
||||
MemberCardRecord record = createRecord();
|
||||
MemberCard card = createCard();
|
||||
when(memberCardRecordRepository.findCardsByMemberIdWithStatus(MEMBER_ID, "active"))
|
||||
.thenReturn(Flux.just(record));
|
||||
when(memberCardRepository.findByIdAndDeletedAtIsNull(MEMBER_CARD_ID)).thenReturn(Mono.just(card));
|
||||
|
||||
Flux<MemberCardRecord> result = memberCardRecordService.findCardsByMemberIdWithStatus(MEMBER_ID, "active");
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(r -> {
|
||||
assertThat(r.getMemberCardName()).isEqualTo("月卡");
|
||||
assertThat(r.getMemberCardType()).isEqualTo("TIME_CARD");
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
|
||||
private MemberCard createStoredValueCard() {
|
||||
MemberCard card = MemberCard.builder()
|
||||
.memberCardId(MEMBER_CARD_ID)
|
||||
.memberCardName("储值卡")
|
||||
.memberCardType("STORED_VALUE_CARD")
|
||||
.memberCardPrice(500.0)
|
||||
.memberCardAmount(500.0)
|
||||
.memberCardStatus(1)
|
||||
.build();
|
||||
card.setId(MEMBER_CARD_ID);
|
||||
return card;
|
||||
}
|
||||
|
||||
// ==================== findPrimaryActiveCardByMemberId ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("findPrimaryActiveCardByMemberId 方法")
|
||||
class FindPrimaryActiveCardByMemberIdTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("无记录时应返回空")
|
||||
void shouldReturnEmptyWhenNoRecords() {
|
||||
when(memberCardRecordRepository.findCardsByMemberIdWithStatus(MEMBER_ID, "active"))
|
||||
.thenReturn(Flux.empty());
|
||||
|
||||
Mono<MemberCardRecord> result = memberCardRecordService.findPrimaryActiveCardByMemberId(MEMBER_ID);
|
||||
|
||||
StepVerifier.create(result).verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应优先返回临期卡(1-3天到期)")
|
||||
void shouldPreferExpiringCard() {
|
||||
MemberCardRecord expiringRecord = createRecord();
|
||||
expiringRecord.setExpireTime(LocalDateTime.now().plusDays(2));
|
||||
MemberCardRecord farRecord = createRecord();
|
||||
farRecord.setMemberCardRecordId(20L);
|
||||
farRecord.setExpireTime(LocalDateTime.now().plusDays(30));
|
||||
|
||||
when(memberCardRecordRepository.findCardsByMemberIdWithStatus(MEMBER_ID, "active"))
|
||||
.thenReturn(Flux.just(farRecord, expiringRecord));
|
||||
when(memberCardRepository.findByIdAndDeletedAtIsNull(MEMBER_CARD_ID)).thenReturn(Mono.just(createCard()));
|
||||
|
||||
Mono<MemberCardRecord> result = memberCardRecordService.findPrimaryActiveCardByMemberId(MEMBER_ID);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(r -> assertThat(r.getMemberCardRecordId()).isEqualTo(RECORD_ID))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("无临期卡时应返回有效卡(剩余>3天)")
|
||||
void shouldReturnActiveCardWhenNoExpiring() {
|
||||
MemberCardRecord farRecord = createRecord();
|
||||
farRecord.setExpireTime(LocalDateTime.now().plusDays(30));
|
||||
MemberCardRecord expiredRecord = createRecord();
|
||||
expiredRecord.setMemberCardRecordId(20L);
|
||||
expiredRecord.setExpireTime(LocalDateTime.now().minusDays(1));
|
||||
|
||||
when(memberCardRecordRepository.findCardsByMemberIdWithStatus(MEMBER_ID, "active"))
|
||||
.thenReturn(Flux.just(expiredRecord, farRecord));
|
||||
when(memberCardRepository.findByIdAndDeletedAtIsNull(MEMBER_CARD_ID)).thenReturn(Mono.just(createCard()));
|
||||
|
||||
Mono<MemberCardRecord> result = memberCardRecordService.findPrimaryActiveCardByMemberId(MEMBER_ID);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(r -> assertThat(r.getMemberCardRecordId()).isEqualTo(RECORD_ID))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("应过滤掉储值卡")
|
||||
void shouldFilterStoredValueCard() {
|
||||
MemberCardRecord svRecord = createRecord();
|
||||
when(memberCardRecordRepository.findCardsByMemberIdWithStatus(MEMBER_ID, "active"))
|
||||
.thenReturn(Flux.just(svRecord));
|
||||
when(memberCardRepository.findByIdAndDeletedAtIsNull(MEMBER_CARD_ID)).thenReturn(Mono.just(createStoredValueCard()));
|
||||
|
||||
Mono<MemberCardRecord> result = memberCardRecordService.findPrimaryActiveCardByMemberId(MEMBER_ID);
|
||||
|
||||
StepVerifier.create(result).verifyComplete();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== validateCountCard ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("validateCountCard 方法")
|
||||
class ValidateCountCardTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应委托给 repository")
|
||||
void shouldDelegateToRepository() {
|
||||
MemberCardRecord record = createRecord();
|
||||
when(memberCardRecordRepository.validateCountCard(RECORD_ID, 1)).thenReturn(Mono.just(record));
|
||||
|
||||
Mono<MemberCardRecord> result = memberCardRecordService.validateCountCard(RECORD_ID, 1);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(r -> assertThat(r.getMemberCardRecordId()).isEqualTo(RECORD_ID))
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== validateStoredCard ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("validateStoredCard 方法")
|
||||
class ValidateStoredCardTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应委托给 repository")
|
||||
void shouldDelegateToRepository() {
|
||||
MemberCardRecord record = createRecord();
|
||||
when(memberCardRecordRepository.validateStoredCard(RECORD_ID, 100.0)).thenReturn(Mono.just(record));
|
||||
|
||||
Mono<MemberCardRecord> result = memberCardRecordService.validateStoredCard(RECORD_ID, 100.0);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(r -> assertThat(r.getMemberCardRecordId()).isEqualTo(RECORD_ID))
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
+620
@@ -0,0 +1,620 @@
|
||||
package cn.novalon.gym.manage.member.service.impl;
|
||||
|
||||
import cn.novalon.gym.manage.common.util.RedisUtil;
|
||||
import cn.novalon.gym.manage.member.entity.MemberCard;
|
||||
import cn.novalon.gym.manage.member.entity.MemberCardRecord;
|
||||
import cn.novalon.gym.manage.member.enums.CardEvent;
|
||||
import cn.novalon.gym.manage.member.enums.MemberCardRecordStatus;
|
||||
import cn.novalon.gym.manage.member.handler.DistributedLockService;
|
||||
import cn.novalon.gym.manage.member.handler.ExpirationReminderService;
|
||||
import cn.novalon.gym.manage.member.handler.MemberCardStateMachine;
|
||||
import cn.novalon.gym.manage.member.handler.RefundSagaHandler;
|
||||
import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository;
|
||||
import cn.novalon.gym.manage.member.repository.MemberCardRepository;
|
||||
import cn.novalon.gym.manage.member.service.IMemberCardTransactionService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
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 org.springframework.data.domain.Pageable;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@DisplayName("MemberCardServiceImpl 单元测试")
|
||||
class MemberCardServiceImplTest {
|
||||
|
||||
@Mock
|
||||
private MemberCardRepository memberCardRepository;
|
||||
|
||||
@Mock
|
||||
private MemberCardRecordRepository recordRepository;
|
||||
|
||||
@Mock
|
||||
private IMemberCardTransactionService transactionService;
|
||||
|
||||
@Mock
|
||||
private MemberCardStateMachine stateMachine;
|
||||
|
||||
@Mock
|
||||
private DistributedLockService distributedLockService;
|
||||
|
||||
@Mock
|
||||
private ExpirationReminderService expirationReminderService;
|
||||
|
||||
@Mock
|
||||
private RefundSagaHandler refundSagaHandler;
|
||||
|
||||
@Mock
|
||||
private RedisUtil redisUtil;
|
||||
|
||||
@InjectMocks
|
||||
private MemberCardServiceImpl memberCardService;
|
||||
|
||||
private static final Long MEMBER_CARD_ID = 1L;
|
||||
private static final Long MEMBER_ID = 100L;
|
||||
private static final Long RECORD_ID = 10L;
|
||||
private static final Long SOURCE_ORDER_ID = 200L;
|
||||
|
||||
private MemberCard createTimeCard() {
|
||||
return MemberCard.builder()
|
||||
.memberCardId(MEMBER_CARD_ID)
|
||||
.memberCardName("月卡")
|
||||
.memberCardType("TIME_CARD")
|
||||
.memberCardPrice(199.0)
|
||||
.memberCardValidityDays(30)
|
||||
.memberCardStatus(1)
|
||||
.build();
|
||||
}
|
||||
|
||||
private MemberCard createCountCard() {
|
||||
return MemberCard.builder()
|
||||
.memberCardId(MEMBER_CARD_ID)
|
||||
.memberCardName("10次卡")
|
||||
.memberCardType("COUNT_CARD")
|
||||
.memberCardPrice(99.0)
|
||||
.memberCardValidityDays(90)
|
||||
.memberCardTotalTimes(10)
|
||||
.memberCardStatus(1)
|
||||
.build();
|
||||
}
|
||||
|
||||
private MemberCard createStoredValueCard() {
|
||||
return MemberCard.builder()
|
||||
.memberCardId(MEMBER_CARD_ID)
|
||||
.memberCardName("储值卡")
|
||||
.memberCardType("STORED_VALUE_CARD")
|
||||
.memberCardPrice(500.0)
|
||||
.memberCardAmount(500.0)
|
||||
.memberCardStatus(1)
|
||||
.build();
|
||||
}
|
||||
|
||||
private MemberCardRecord createActiveRecord(MemberCard card, MemberCardRecordStatus status) {
|
||||
MemberCardRecord record = new MemberCardRecord();
|
||||
record.setMemberCardRecordId(RECORD_ID);
|
||||
record.setMemberId(MEMBER_ID);
|
||||
record.setMemberCardId(MEMBER_CARD_ID);
|
||||
record.setSourceOrderId(SOURCE_ORDER_ID);
|
||||
record.setStatus(status);
|
||||
record.setPurchaseTime(LocalDateTime.now().minusDays(1));
|
||||
|
||||
if ("TIME_CARD".equals(card.getMemberCardType())) {
|
||||
record.setExpireTime(LocalDateTime.now().plusDays(29));
|
||||
record.setRemainingTimes(0);
|
||||
record.setRemainingAmount(0.0);
|
||||
} else if ("COUNT_CARD".equals(card.getMemberCardType())) {
|
||||
record.setExpireTime(LocalDateTime.now().plusDays(89));
|
||||
record.setRemainingTimes(card.getMemberCardTotalTimes());
|
||||
record.setRemainingAmount(0.0);
|
||||
} else if ("STORED_VALUE_CARD".equals(card.getMemberCardType())) {
|
||||
record.setExpireTime(LocalDateTime.now().plusYears(1));
|
||||
record.setRemainingTimes(0);
|
||||
record.setRemainingAmount(card.getMemberCardAmount());
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// 模拟分布式锁:直接执行锁内的业务逻辑
|
||||
lenient().when(distributedLockService.executeWithLock(anyString(), anyString(), any()))
|
||||
.thenAnswer(invocation -> {
|
||||
Mono<?> bizLogic = invocation.getArgument(2);
|
||||
return bizLogic;
|
||||
});
|
||||
// 模拟交易记录创建
|
||||
lenient().when(transactionService.createTransaction(any())).thenReturn(Mono.empty());
|
||||
// 模拟过期提醒调度
|
||||
lenient().when(expirationReminderService.scheduleExpirationReminder(any())).thenReturn(Mono.empty());
|
||||
// 模拟状态机验证
|
||||
lenient().when(stateMachine.validateTransition(any(), any())).thenReturn(Mono.empty());
|
||||
// 模拟前端重复购买检测:默认返回空,即无重复
|
||||
lenient().when(recordRepository.findRecentActivePurchase(anyLong(), anyLong(), any(LocalDateTime.class)))
|
||||
.thenReturn(Mono.empty());
|
||||
}
|
||||
|
||||
// ==================== findByMemberCardIdAndDeletedAtIsNull ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("findByMemberCardIdAndDeletedAtIsNull 方法")
|
||||
class FindByMemberCardIdTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("缓存未命中时应从数据库查询并更新缓存")
|
||||
void shouldQueryDatabaseWhenCacheMiss() {
|
||||
MemberCard card = createTimeCard();
|
||||
when(redisUtil.get("member:card:" + MEMBER_CARD_ID)).thenReturn(Mono.empty());
|
||||
when(memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(MEMBER_CARD_ID))
|
||||
.thenReturn(Mono.just(card));
|
||||
|
||||
Mono<MemberCard> result = memberCardService.findByMemberCardIdAndDeletedAtIsNull(MEMBER_CARD_ID);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(c -> assertThat(c.getMemberCardName()).isEqualTo("月卡"))
|
||||
.verifyComplete();
|
||||
|
||||
verify(memberCardRepository).findByMemberCardIdAndDeletedAtIsNull(MEMBER_CARD_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("卡片不存在时应返回空")
|
||||
void shouldReturnEmptyWhenCardNotFound() {
|
||||
when(redisUtil.get("member:card:" + MEMBER_CARD_ID)).thenReturn(Mono.empty());
|
||||
when(memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(MEMBER_CARD_ID))
|
||||
.thenReturn(Mono.empty());
|
||||
|
||||
Mono<MemberCard> result = memberCardService.findByMemberCardIdAndDeletedAtIsNull(MEMBER_CARD_ID);
|
||||
|
||||
StepVerifier.create(result).verifyComplete();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== findWithConditions ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("findWithConditions 方法")
|
||||
class FindWithConditionsTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应正确委托给 repository")
|
||||
void shouldDelegateToRepository() {
|
||||
MemberCard card = createTimeCard();
|
||||
Pageable pageable = Pageable.ofSize(10);
|
||||
when(memberCardRepository.findWithConditions(1, "月卡", "TIME_CARD", 100.0, 300.0, pageable))
|
||||
.thenReturn(Flux.just(card));
|
||||
|
||||
Flux<MemberCard> result = memberCardService.findWithConditions(1, "月卡", "TIME_CARD", 100.0, 300.0, pageable);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(c -> assertThat(c.getMemberCardName()).isEqualTo("月卡"))
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== save ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("save 方法")
|
||||
class SaveTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("成功保存时应清除缓存")
|
||||
void shouldClearCacheWhenSaved() {
|
||||
MemberCard card = createTimeCard();
|
||||
when(memberCardRepository.save(card)).thenReturn(Mono.just(card));
|
||||
when(redisUtil.delete("member:card:" + MEMBER_CARD_ID)).thenReturn(Mono.just(1L));
|
||||
|
||||
Mono<MemberCard> result = memberCardService.save(card);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(c -> assertThat(c.getMemberCardName()).isEqualTo("月卡"))
|
||||
.verifyComplete();
|
||||
|
||||
verify(redisUtil).delete("member:card:" + MEMBER_CARD_ID);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== purchaseCard ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("purchaseCard 方法")
|
||||
class PurchaseCardTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("memberId 为 null 时应抛出错误")
|
||||
void shouldThrowWhenMemberIdNull() {
|
||||
Mono<MemberCardRecord> result = memberCardService.purchaseCard(null, MEMBER_CARD_ID, SOURCE_ORDER_ID);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectErrorMatches(e -> e instanceof RuntimeException
|
||||
&& e.getMessage().equals("会员ID不能为空"))
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("memberCardId 为 null 时应抛出错误")
|
||||
void shouldThrowWhenCardIdNull() {
|
||||
Mono<MemberCardRecord> result = memberCardService.purchaseCard(MEMBER_ID, null, SOURCE_ORDER_ID);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectErrorMatches(e -> e instanceof RuntimeException
|
||||
&& e.getMessage().equals("会员卡类型ID不能为空"))
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("有 sourceOrderId 时幂等返回已存在的记录")
|
||||
void shouldReturnExistingRecordWhenSourceOrderIdExists() {
|
||||
MemberCardRecord existingRecord = createActiveRecord(createTimeCard(), MemberCardRecordStatus.ACTIVE);
|
||||
when(recordRepository.findBySourceOrderId(SOURCE_ORDER_ID)).thenReturn(Mono.just(existingRecord));
|
||||
|
||||
Mono<MemberCardRecord> result = memberCardService.purchaseCard(MEMBER_ID, MEMBER_CARD_ID, SOURCE_ORDER_ID);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(r -> assertThat(r.getMemberCardRecordId()).isEqualTo(RECORD_ID))
|
||||
.verifyComplete();
|
||||
|
||||
verify(recordRepository).findBySourceOrderId(SOURCE_ORDER_ID);
|
||||
verifyNoMoreInteractions(recordRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("购买时间卡应成功创建记录")
|
||||
void shouldPurchaseTimeCardSuccessfully() {
|
||||
MemberCard timeCard = createTimeCard();
|
||||
lenient().when(redisUtil.get("member:card:" + MEMBER_CARD_ID)).thenReturn(Mono.empty());
|
||||
when(memberCardRepository.findByIdAndDeletedAtIsNull(MEMBER_CARD_ID)).thenReturn(Mono.just(timeCard));
|
||||
when(recordRepository.insertActiveRecord(anyLong(), anyLong(), any(), anyInt(), anyDouble(), any()))
|
||||
.thenReturn(Mono.just(createActiveRecord(timeCard, MemberCardRecordStatus.ACTIVE)));
|
||||
|
||||
Mono<MemberCardRecord> result = memberCardService.purchaseCard(MEMBER_ID, MEMBER_CARD_ID, null);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(r -> {
|
||||
assertThat(r.getMemberId()).isEqualTo(MEMBER_ID);
|
||||
assertThat(r.getMemberCardId()).isEqualTo(MEMBER_CARD_ID);
|
||||
})
|
||||
.verifyComplete();
|
||||
|
||||
verify(memberCardRepository).findByIdAndDeletedAtIsNull(MEMBER_CARD_ID);
|
||||
verify(recordRepository).insertActiveRecord(anyLong(), anyLong(), any(), anyInt(), anyDouble(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("购买已下架卡时应抛出错误")
|
||||
void shouldThrowWhenCardOffShelf() {
|
||||
MemberCard offShelfCard = createTimeCard();
|
||||
offShelfCard.setMemberCardStatus(0);
|
||||
when(memberCardRepository.findByIdAndDeletedAtIsNull(MEMBER_CARD_ID)).thenReturn(Mono.just(offShelfCard));
|
||||
|
||||
Mono<MemberCardRecord> result = memberCardService.purchaseCard(MEMBER_ID, MEMBER_CARD_ID, null);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectErrorMatches(e -> e instanceof RuntimeException
|
||||
&& e.getMessage().equals("该会员卡已下架"))
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("卡类型不存在时应抛出错误")
|
||||
void shouldThrowWhenCardNotFound() {
|
||||
when(memberCardRepository.findByIdAndDeletedAtIsNull(MEMBER_CARD_ID)).thenReturn(Mono.empty());
|
||||
|
||||
Mono<MemberCardRecord> result = memberCardService.purchaseCard(MEMBER_ID, MEMBER_CARD_ID, null);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectErrorMatches(e -> e instanceof RuntimeException
|
||||
&& e.getMessage().equals("会员卡类型不存在"))
|
||||
.verify();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== useCard ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("useCard 方法")
|
||||
class UseCardTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("记录不存在时应抛出错误")
|
||||
void shouldThrowWhenRecordNotFound() {
|
||||
when(recordRepository.findById(RECORD_ID)).thenReturn(Mono.empty());
|
||||
|
||||
Mono<MemberCardRecord> result = memberCardService.useCard(RECORD_ID, MEMBER_ID, 1, 0.0);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectErrorMatches(e -> e instanceof RuntimeException
|
||||
&& e.getMessage().equals("会员卡记录不存在"))
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("无权操作时应抛出错误")
|
||||
void shouldThrowWhenUnauthorized() {
|
||||
MemberCardRecord record = createActiveRecord(createTimeCard(), MemberCardRecordStatus.ACTIVE);
|
||||
when(recordRepository.findById(RECORD_ID)).thenReturn(Mono.just(record));
|
||||
|
||||
Mono<MemberCardRecord> result = memberCardService.useCard(RECORD_ID, 999L, 1, 0.0);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectErrorMatches(e -> e instanceof RuntimeException
|
||||
&& e.getMessage().equals("无权操作此会员卡"))
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("使用次卡应扣除次数")
|
||||
void shouldDeductTimesForCountCard() {
|
||||
MemberCard countCard = createCountCard();
|
||||
MemberCardRecord record = createActiveRecord(countCard, MemberCardRecordStatus.ACTIVE);
|
||||
when(recordRepository.findById(RECORD_ID)).thenReturn(Mono.just(record));
|
||||
when(memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(MEMBER_CARD_ID)).thenReturn(Mono.just(countCard));
|
||||
when(recordRepository.save(any(MemberCardRecord.class))).thenAnswer(inv -> Mono.just(inv.getArgument(0)));
|
||||
|
||||
Mono<MemberCardRecord> result = memberCardService.useCard(RECORD_ID, MEMBER_ID, 1, 0.0);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(r -> {
|
||||
assertThat(r.getRemainingTimes()).isEqualTo(9);
|
||||
assertThat(r.getStatus()).isEqualTo(MemberCardRecordStatus.ACTIVE);
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("次卡次数耗尽时应标记为 USED_UP")
|
||||
void shouldMarkUsedUpWhenCountCardExhausted() {
|
||||
MemberCard countCard = createCountCard();
|
||||
MemberCardRecord record = createActiveRecord(countCard, MemberCardRecordStatus.ACTIVE);
|
||||
record.setRemainingTimes(1);
|
||||
when(recordRepository.findById(RECORD_ID)).thenReturn(Mono.just(record));
|
||||
when(memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(MEMBER_CARD_ID)).thenReturn(Mono.just(countCard));
|
||||
when(recordRepository.save(any(MemberCardRecord.class))).thenAnswer(inv -> Mono.just(inv.getArgument(0)));
|
||||
|
||||
Mono<MemberCardRecord> result = memberCardService.useCard(RECORD_ID, MEMBER_ID, 1, 0.0);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(r -> {
|
||||
assertThat(r.getRemainingTimes()).isZero();
|
||||
assertThat(r.getStatus()).isEqualTo(MemberCardRecordStatus.USED_UP);
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("次数不足时应抛出错误")
|
||||
void shouldThrowWhenInsufficientTimes() {
|
||||
MemberCard countCard = createCountCard();
|
||||
MemberCardRecord record = createActiveRecord(countCard, MemberCardRecordStatus.ACTIVE);
|
||||
record.setRemainingTimes(0);
|
||||
when(recordRepository.findById(RECORD_ID)).thenReturn(Mono.just(record));
|
||||
when(memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(MEMBER_CARD_ID)).thenReturn(Mono.just(countCard));
|
||||
|
||||
Mono<MemberCardRecord> result = memberCardService.useCard(RECORD_ID, MEMBER_ID, 1, 0.0);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectErrorMatches(e -> e instanceof RuntimeException
|
||||
&& e.getMessage().equals("剩余次数不足"))
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("使用储值卡应扣除金额")
|
||||
void shouldDeductAmountForStoredValueCard() {
|
||||
MemberCard svCard = createStoredValueCard();
|
||||
MemberCardRecord record = createActiveRecord(svCard, MemberCardRecordStatus.ACTIVE);
|
||||
when(recordRepository.findById(RECORD_ID)).thenReturn(Mono.just(record));
|
||||
when(memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(MEMBER_CARD_ID)).thenReturn(Mono.just(svCard));
|
||||
when(recordRepository.save(any(MemberCardRecord.class))).thenAnswer(inv -> Mono.just(inv.getArgument(0)));
|
||||
|
||||
Mono<MemberCardRecord> result = memberCardService.useCard(RECORD_ID, MEMBER_ID, 0, 50.0);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(r -> {
|
||||
assertThat(r.getRemainingAmount()).isEqualTo(450.0);
|
||||
assertThat(r.getStatus()).isEqualTo(MemberCardRecordStatus.ACTIVE);
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("余额不足时应抛出错误")
|
||||
void shouldThrowWhenInsufficientBalance() {
|
||||
MemberCard svCard = createStoredValueCard();
|
||||
MemberCardRecord record = createActiveRecord(svCard, MemberCardRecordStatus.ACTIVE);
|
||||
record.setRemainingAmount(10.0);
|
||||
when(recordRepository.findById(RECORD_ID)).thenReturn(Mono.just(record));
|
||||
when(memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(MEMBER_CARD_ID)).thenReturn(Mono.just(svCard));
|
||||
|
||||
Mono<MemberCardRecord> result = memberCardService.useCard(RECORD_ID, MEMBER_ID, 0, 50.0);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectErrorMatches(e -> e instanceof RuntimeException
|
||||
&& e.getMessage().equals("余额不足"))
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("使用时间卡应检查有效期")
|
||||
void shouldCheckExpiryForTimeCard() {
|
||||
MemberCard timeCard = createTimeCard();
|
||||
MemberCardRecord record = createActiveRecord(timeCard, MemberCardRecordStatus.ACTIVE);
|
||||
record.setExpireTime(LocalDateTime.now().minusDays(1));
|
||||
when(recordRepository.findById(RECORD_ID)).thenReturn(Mono.just(record));
|
||||
when(memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(MEMBER_CARD_ID)).thenReturn(Mono.just(timeCard));
|
||||
|
||||
Mono<MemberCardRecord> result = memberCardService.useCard(RECORD_ID, MEMBER_ID, 0, 0.0);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectErrorMatches(e -> e instanceof RuntimeException
|
||||
&& e.getMessage().equals("会员卡已过期"))
|
||||
.verify();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== renewCard ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("renewCard 方法")
|
||||
class RenewCardTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("续费时间卡应延长有效期")
|
||||
void shouldRenewTimeCard() {
|
||||
MemberCard timeCard = createTimeCard();
|
||||
MemberCardRecord record = createActiveRecord(timeCard, MemberCardRecordStatus.ACTIVE);
|
||||
when(recordRepository.findById(RECORD_ID)).thenReturn(Mono.just(record));
|
||||
when(stateMachine.validateTransition(record, CardEvent.RENEW)).thenReturn(Mono.empty());
|
||||
when(memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(MEMBER_CARD_ID)).thenReturn(Mono.just(timeCard));
|
||||
when(recordRepository.save(any(MemberCardRecord.class))).thenAnswer(inv -> Mono.just(inv.getArgument(0)));
|
||||
|
||||
Mono<MemberCardRecord> result = memberCardService.renewCard(RECORD_ID, MEMBER_ID, null, null, null, null);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(r -> {
|
||||
assertThat(r.getStatus()).isEqualTo(MemberCardRecordStatus.ACTIVE);
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("无权续费时应抛出错误")
|
||||
void shouldThrowWhenUnauthorized() {
|
||||
MemberCardRecord record = createActiveRecord(createTimeCard(), MemberCardRecordStatus.ACTIVE);
|
||||
when(recordRepository.findById(RECORD_ID)).thenReturn(Mono.just(record));
|
||||
|
||||
Mono<MemberCardRecord> result = memberCardService.renewCard(RECORD_ID, 999L, null, null, null, null);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectErrorMatches(e -> e instanceof RuntimeException
|
||||
&& e.getMessage().equals("无权操作此会员卡"))
|
||||
.verify();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== refundCard ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("refundCard 方法")
|
||||
class RefundCardTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("记录不存在时应抛出错误")
|
||||
void shouldThrowWhenRecordNotFound() {
|
||||
when(recordRepository.findById(RECORD_ID)).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Void> result = memberCardService.refundCard(RECORD_ID, MEMBER_ID);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectErrorMatches(e -> e instanceof RuntimeException
|
||||
&& e.getMessage().equals("会员卡记录不存在"))
|
||||
.verify();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== processExpiredCards ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("processExpiredCards 方法")
|
||||
class ProcessExpiredCardsTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应处理过期卡并返回处理数量")
|
||||
void shouldProcessExpiredCards() {
|
||||
MemberCardRecord expiredRecord = createActiveRecord(createTimeCard(), MemberCardRecordStatus.ACTIVE);
|
||||
expiredRecord.setExpireTime(LocalDateTime.now().minusDays(1));
|
||||
when(recordRepository.findExpiredCards()).thenReturn(Flux.just(expiredRecord));
|
||||
when(stateMachine.transition(expiredRecord.getStatus(), CardEvent.EXPIRE))
|
||||
.thenReturn(Mono.just(MemberCardRecordStatus.EXPIRED));
|
||||
when(recordRepository.updateStatus(RECORD_ID, MemberCardRecordStatus.EXPIRED.name()))
|
||||
.thenReturn(Mono.just(1));
|
||||
|
||||
Mono<Integer> result = memberCardService.processExpiredCards();
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(count -> assertThat(count).isEqualTo(1))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("无过期卡时应返回0")
|
||||
void shouldReturnZeroWhenNoExpiredCards() {
|
||||
when(recordRepository.findExpiredCards()).thenReturn(Flux.empty());
|
||||
|
||||
Mono<Integer> result = memberCardService.processExpiredCards();
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(count -> assertThat(count).isZero())
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 其他简单委托方法 ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("委托方法")
|
||||
class DelegationMethodsTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("countWithConditions 应委托给 repository")
|
||||
void shouldDelegateCountWithConditions() {
|
||||
when(memberCardRepository.countWithConditions(1, null, null, null, null))
|
||||
.thenReturn(Mono.just(10L));
|
||||
|
||||
Mono<Long> result = memberCardService.countWithConditions(1, null, null, null, null);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(count -> assertThat(count).isEqualTo(10L))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("existsPurchasedRecord 应委托给 repository")
|
||||
void shouldDelegateExistsPurchasedRecord() {
|
||||
when(memberCardRepository.existsPurchasedRecord(MEMBER_CARD_ID)).thenReturn(Mono.just(true));
|
||||
|
||||
Mono<Boolean> result = memberCardService.existsPurchasedRecord(MEMBER_CARD_ID);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(exists -> assertThat(exists).isTrue())
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("logicalDelete 应委托给 repository")
|
||||
void shouldDelegateLogicalDelete() {
|
||||
when(memberCardRepository.logicalDelete(MEMBER_CARD_ID)).thenReturn(Mono.just(1));
|
||||
|
||||
Mono<Integer> result = memberCardService.logicalDelete(MEMBER_CARD_ID);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(count -> assertThat(count).isEqualTo(1))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("findActiveCards 应委托给 repository")
|
||||
void shouldDelegateFindActiveCards() {
|
||||
MemberCard card = createTimeCard();
|
||||
when(memberCardRepository.findActiveCards(1)).thenReturn(Flux.just(card));
|
||||
|
||||
Flux<MemberCard> result = memberCardService.findActiveCards(1);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(c -> assertThat(c.getMemberCardName()).isEqualTo("月卡"))
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
+317
@@ -0,0 +1,317 @@
|
||||
package cn.novalon.gym.manage.member.service.impl;
|
||||
|
||||
import cn.novalon.gym.manage.member.entity.MemberCardTransaction;
|
||||
import cn.novalon.gym.manage.member.enums.TransactionType;
|
||||
import cn.novalon.gym.manage.member.repository.MemberCardTransactionRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
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 org.springframework.data.domain.Pageable;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@DisplayName("MemberCardTransactionServiceImpl 单元测试")
|
||||
class MemberCardTransactionServiceImplTest {
|
||||
|
||||
@Mock
|
||||
private MemberCardTransactionRepository transactionRepository;
|
||||
|
||||
@InjectMocks
|
||||
private MemberCardTransactionServiceImpl transactionService;
|
||||
|
||||
private static final Long RECORD_ID = 10L;
|
||||
private static final Long MEMBER_ID = 100L;
|
||||
private static final Long MEMBER_CARD_ID = 1L;
|
||||
private static final Long SOURCE_ORDER_ID = 200L;
|
||||
|
||||
private MemberCardTransaction createTransaction() {
|
||||
return MemberCardTransaction.builder()
|
||||
.memberCardRecordId(RECORD_ID)
|
||||
.memberCardId(MEMBER_CARD_ID)
|
||||
.memberId(MEMBER_ID)
|
||||
.operationType("PURCHASE")
|
||||
.changeAmount(10)
|
||||
.changeBalance(-199.0)
|
||||
.afterRemainingCount(10)
|
||||
.afterRemainingBalance(0.0)
|
||||
.sourceOrderId(SOURCE_ORDER_ID)
|
||||
.remark("购买会员卡")
|
||||
.build();
|
||||
}
|
||||
|
||||
// ==================== insertTransaction ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("insertTransaction 方法")
|
||||
class InsertTransactionTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应委托给 repository")
|
||||
void shouldDelegateToRepository() {
|
||||
MemberCardTransaction tx = createTransaction();
|
||||
when(transactionRepository.insertTransaction(
|
||||
tx.getMemberCardRecordId(), tx.getMemberCardId(), tx.getMemberId(),
|
||||
tx.getOperationType(), tx.getChangeAmount(), tx.getChangeBalance(),
|
||||
tx.getAfterRemainingCount(), tx.getAfterRemainingBalance(),
|
||||
tx.getRelatedBizType(), tx.getSourceOrderId(), tx.getRemark()))
|
||||
.thenReturn(Mono.just(tx));
|
||||
|
||||
Mono<MemberCardTransaction> result = transactionService.insertTransaction(tx);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(t -> assertThat(t.getMemberCardRecordId()).isEqualTo(RECORD_ID))
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== createTransaction ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("createTransaction 方法")
|
||||
class CreateTransactionTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应委托给 repository 并返回 Void")
|
||||
void shouldDelegateToRepository() {
|
||||
MemberCardTransaction tx = createTransaction();
|
||||
when(transactionRepository.insertTransaction(
|
||||
tx.getMemberCardRecordId(), tx.getMemberCardId(), tx.getMemberId(),
|
||||
tx.getOperationType(), tx.getChangeAmount(), tx.getChangeBalance(),
|
||||
tx.getAfterRemainingCount(), tx.getAfterRemainingBalance(),
|
||||
tx.getRelatedBizType(), tx.getSourceOrderId(), tx.getRemark()))
|
||||
.thenReturn(Mono.just(tx));
|
||||
|
||||
Mono<Void> result = transactionService.createTransaction(tx);
|
||||
|
||||
StepVerifier.create(result).verifyComplete();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== findByMemberIdAndTimeRange ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("findByMemberIdAndTimeRange 方法")
|
||||
class FindByMemberIdAndTimeRangeTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应委托给 repository")
|
||||
void shouldDelegateToRepository() {
|
||||
MemberCardTransaction tx = createTransaction();
|
||||
LocalDateTime start = LocalDateTime.now().minusDays(30);
|
||||
LocalDateTime end = LocalDateTime.now();
|
||||
Pageable pageable = Pageable.ofSize(10);
|
||||
when(transactionRepository.findByMemberIdAndTimeRange(MEMBER_ID, start, end, pageable))
|
||||
.thenReturn(Flux.just(tx));
|
||||
|
||||
Flux<MemberCardTransaction> result = transactionService.findByMemberIdAndTimeRange(MEMBER_ID, start, end, pageable);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(t -> assertThat(t.getMemberCardRecordId()).isEqualTo(RECORD_ID))
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== findWithConditions ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("findWithConditions 方法")
|
||||
class FindWithConditionsTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("有 operationType 时应转换并委托")
|
||||
void shouldConvertTypeAndDelegate() {
|
||||
MemberCardTransaction tx = createTransaction();
|
||||
LocalDateTime start = LocalDateTime.now().minusDays(30);
|
||||
LocalDateTime end = LocalDateTime.now();
|
||||
Pageable pageable = Pageable.ofSize(10);
|
||||
when(transactionRepository.findWithConditions(MEMBER_ID, MEMBER_CARD_ID, "PURCHASE", start, end, pageable))
|
||||
.thenReturn(Flux.just(tx));
|
||||
|
||||
Flux<MemberCardTransaction> result = transactionService.findWithConditions(
|
||||
MEMBER_ID, MEMBER_CARD_ID, TransactionType.PURCHASE, start, end, pageable);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(t -> assertThat(t.getMemberCardRecordId()).isEqualTo(RECORD_ID))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("operationType 为 null 时应传 null")
|
||||
void shouldPassNullWhenTypeIsNull() {
|
||||
MemberCardTransaction tx = createTransaction();
|
||||
LocalDateTime start = LocalDateTime.now().minusDays(30);
|
||||
LocalDateTime end = LocalDateTime.now();
|
||||
Pageable pageable = Pageable.ofSize(10);
|
||||
when(transactionRepository.findWithConditions(MEMBER_ID, MEMBER_CARD_ID, null, start, end, pageable))
|
||||
.thenReturn(Flux.just(tx));
|
||||
|
||||
Flux<MemberCardTransaction> result = transactionService.findWithConditions(
|
||||
MEMBER_ID, MEMBER_CARD_ID, null, start, end, pageable);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(t -> assertThat(t.getMemberCardRecordId()).isEqualTo(RECORD_ID))
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== countWithConditions ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("countWithConditions 方法")
|
||||
class CountWithConditionsTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("有 operationType 时应转换并委托")
|
||||
void shouldConvertTypeAndDelegate() {
|
||||
LocalDateTime start = LocalDateTime.now().minusDays(30);
|
||||
LocalDateTime end = LocalDateTime.now();
|
||||
when(transactionRepository.countWithConditions(MEMBER_ID, MEMBER_CARD_ID, "PURCHASE", start, end))
|
||||
.thenReturn(Mono.just(5L));
|
||||
|
||||
Mono<Long> result = transactionService.countWithConditions(
|
||||
MEMBER_ID, MEMBER_CARD_ID, TransactionType.PURCHASE, start, end);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(count -> assertThat(count).isEqualTo(5L))
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== findByMemberCardId ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("findByMemberCardId 方法")
|
||||
class FindByMemberCardIdTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应委托给 repository")
|
||||
void shouldDelegateToRepository() {
|
||||
MemberCardTransaction tx = createTransaction();
|
||||
when(transactionRepository.findByMemberCardId(MEMBER_CARD_ID)).thenReturn(Flux.just(tx));
|
||||
|
||||
Flux<MemberCardTransaction> result = transactionService.findByMemberCardId(MEMBER_CARD_ID);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(t -> assertThat(t.getMemberCardRecordId()).isEqualTo(RECORD_ID))
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== findByMemberId ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("findByMemberId 方法")
|
||||
class FindByMemberIdTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应委托给 repository")
|
||||
void shouldDelegateToRepository() {
|
||||
MemberCardTransaction tx = createTransaction();
|
||||
when(transactionRepository.findByMemberId(MEMBER_ID)).thenReturn(Flux.just(tx));
|
||||
|
||||
Flux<MemberCardTransaction> result = transactionService.findByMemberId(MEMBER_ID);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(t -> assertThat(t.getMemberCardRecordId()).isEqualTo(RECORD_ID))
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== findByRecordId ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("findByRecordId 方法")
|
||||
class FindByRecordIdTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应委托给 repository")
|
||||
void shouldDelegateToRepository() {
|
||||
MemberCardTransaction tx = createTransaction();
|
||||
when(transactionRepository.findByRecordId(RECORD_ID)).thenReturn(Flux.just(tx));
|
||||
|
||||
Flux<MemberCardTransaction> result = transactionService.findByRecordId(RECORD_ID);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(t -> assertThat(t.getMemberCardRecordId()).isEqualTo(RECORD_ID))
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== sumDeductCountByCardId ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("sumDeductCountByCardId 方法")
|
||||
class SumDeductCountByCardIdTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应委托给 repository")
|
||||
void shouldDelegateToRepository() {
|
||||
LocalDateTime start = LocalDateTime.now().minusDays(30);
|
||||
LocalDateTime end = LocalDateTime.now();
|
||||
when(transactionRepository.sumDeductCountByCardId(MEMBER_CARD_ID, start, end))
|
||||
.thenReturn(Mono.just(10L));
|
||||
|
||||
Mono<Long> result = transactionService.sumDeductCountByCardId(MEMBER_CARD_ID, start, end);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(count -> assertThat(count).isEqualTo(10L))
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== sumRenewAmountByTimeRange ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("sumRenewAmountByTimeRange 方法")
|
||||
class SumRenewAmountByTimeRangeTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应委托给 repository")
|
||||
void shouldDelegateToRepository() {
|
||||
LocalDateTime start = LocalDateTime.now().minusDays(30);
|
||||
LocalDateTime end = LocalDateTime.now();
|
||||
when(transactionRepository.sumRenewAmountByTimeRange(start, end)).thenReturn(Mono.just(500.0));
|
||||
|
||||
Mono<Double> result = transactionService.sumRenewAmountByTimeRange(start, end);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(amount -> assertThat(amount).isEqualTo(500.0))
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== sumPurchaseAmountByMemberId ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("sumPurchaseAmountByMemberId 方法")
|
||||
class SumPurchaseAmountByMemberIdTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("应委托给 repository")
|
||||
void shouldDelegateToRepository() {
|
||||
LocalDateTime start = LocalDateTime.now().minusDays(30);
|
||||
LocalDateTime end = LocalDateTime.now();
|
||||
when(transactionRepository.sumPurchaseAmountByMemberId(MEMBER_ID, start, end))
|
||||
.thenReturn(Mono.just(199.0));
|
||||
|
||||
Mono<Double> result = transactionService.sumPurchaseAmountByMemberId(MEMBER_ID, start, end);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(amount -> assertThat(amount).isEqualTo(199.0))
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
+633
@@ -0,0 +1,633 @@
|
||||
package cn.novalon.gym.manage.member.service.impl;
|
||||
|
||||
import cn.novalon.gym.manage.common.exception.ConflictException;
|
||||
import cn.novalon.gym.manage.common.exception.ErrorCode;
|
||||
import cn.novalon.gym.manage.common.exception.NotFoundException;
|
||||
import cn.novalon.gym.manage.common.util.HtmlEscapeUtil;
|
||||
import cn.novalon.gym.manage.common.util.RedisUtil;
|
||||
import cn.novalon.gym.manage.member.dto.SearchMemberDto;
|
||||
import cn.novalon.gym.manage.member.dto.UpdateMemberInfoDto;
|
||||
import cn.novalon.gym.manage.member.entity.Member;
|
||||
import cn.novalon.gym.manage.member.enums.GenderEnum;
|
||||
import cn.novalon.gym.manage.member.es.entity.MemberES;
|
||||
import cn.novalon.gym.manage.member.es.repository.MemberESRepository;
|
||||
import cn.novalon.gym.manage.member.repository.IMemberRepository;
|
||||
import cn.novalon.gym.manage.member.util.AesUtil;
|
||||
import cn.novalon.gym.manage.member.util.EsSyncUtils;
|
||||
import cn.novalon.gym.manage.member.vo.MemberCardInfoVO;
|
||||
import cn.novalon.gym.manage.member.vo.MemberDetailVO;
|
||||
import cn.novalon.gym.manage.member.vo.MemberInfoVO;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@DisplayName("MemberServiceImpl 单元测试")
|
||||
class MemberServiceImplTest {
|
||||
|
||||
@Mock
|
||||
private IMemberRepository memberRepository;
|
||||
|
||||
@Mock
|
||||
private MemberESRepository memberESRepository;
|
||||
|
||||
@Mock
|
||||
private EsSyncUtils esSyncUtils;
|
||||
|
||||
@Mock
|
||||
private RedisUtil redisUtil;
|
||||
|
||||
@InjectMocks
|
||||
private MemberServiceImpl memberServiceImpl;
|
||||
|
||||
private EsSyncUtils.EntitySyncer<Member, MemberES, String> mockSyncer;
|
||||
|
||||
private static final Long MEMBER_ID = 10001L;
|
||||
private static final String TEST_PHONE = "13812345678";
|
||||
private static final String ENCRYPTED_PHONE = "encrypted_phone_value";
|
||||
private static final String DECRYPTED_PHONE = "13812345678";
|
||||
private static final String MASKED_PHONE = "138****5678";
|
||||
private static final String CACHE_INFO_KEY = "member:info:" + MEMBER_ID;
|
||||
private static final String CACHE_DETAIL_KEY = "member:detail:" + MEMBER_ID;
|
||||
|
||||
@BeforeEach
|
||||
@SuppressWarnings("unchecked")
|
||||
void setUp() {
|
||||
// 模拟 EsSyncUtils.bind() 返回 mock EntitySyncer
|
||||
mockSyncer = mock(EsSyncUtils.EntitySyncer.class);
|
||||
lenient().doReturn(mockSyncer).when(esSyncUtils).bind(any(), any(), any());
|
||||
// 调用 @PostConstruct init()
|
||||
memberServiceImpl.init();
|
||||
}
|
||||
|
||||
// ==================== 辅助方法 ====================
|
||||
|
||||
private Member createTestMember() {
|
||||
Member member = Member.builder()
|
||||
.memberNo("M20260001")
|
||||
.nickname("测试会员")
|
||||
.phone(ENCRYPTED_PHONE)
|
||||
.gender(1)
|
||||
.birthday(LocalDate.of(1995, 1, 1))
|
||||
.avatar("https://example.com/avatar.png")
|
||||
.subscribed(true)
|
||||
.lastLoginAt(LocalDateTime.of(2026, 1, 1, 10, 0))
|
||||
.build();
|
||||
member.setId(MEMBER_ID);
|
||||
return member;
|
||||
}
|
||||
|
||||
private MemberInfoVO createTestMemberInfoVO() {
|
||||
return MemberInfoVO.builder()
|
||||
.id(MEMBER_ID)
|
||||
.memberNo("M20260001")
|
||||
.nickname("测试会员")
|
||||
.phone(MASKED_PHONE)
|
||||
.gender(GenderEnum.MALE)
|
||||
.genderDesc("男")
|
||||
.birthday(LocalDate.of(1995, 1, 1))
|
||||
.avatar("https://example.com/avatar.png")
|
||||
.hasPhone(true)
|
||||
.isSubscribed(true)
|
||||
.lastLoginAt(LocalDateTime.of(2026, 1, 1, 10, 0))
|
||||
.build();
|
||||
}
|
||||
|
||||
private UpdateMemberInfoDto createUpdateDto() {
|
||||
UpdateMemberInfoDto dto = new UpdateMemberInfoDto();
|
||||
dto.setNickname("新昵称");
|
||||
dto.setGender(GenderEnum.FEMALE);
|
||||
dto.setBirthday(LocalDate.of(1996, 2, 2));
|
||||
dto.setAvatar("https://example.com/new-avatar.png");
|
||||
dto.setAddress("北京市朝阳区");
|
||||
return dto;
|
||||
}
|
||||
|
||||
private MemberCardInfoVO createTestCardInfoVO() {
|
||||
return MemberCardInfoVO.builder()
|
||||
.id(1L)
|
||||
.memberCardId(1L)
|
||||
.memberCardName("月卡")
|
||||
.memberCardType("TIME_CARD")
|
||||
.memberCardStatus(1)
|
||||
.build();
|
||||
}
|
||||
|
||||
// ==================== getMemberInfo ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("getMemberInfo 方法")
|
||||
class GetMemberInfoTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("缓存命中时应直接返回缓存数据")
|
||||
void shouldReturnCachedDataWhenCacheHit() {
|
||||
MemberInfoVO cachedVO = createTestMemberInfoVO();
|
||||
when(redisUtil.get(CACHE_INFO_KEY, MemberInfoVO.class)).thenReturn(Mono.just(cachedVO));
|
||||
|
||||
Mono<MemberInfoVO> result = memberServiceImpl.getMemberInfo(MEMBER_ID);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(vo -> {
|
||||
assertThat(vo.getId()).isEqualTo(MEMBER_ID);
|
||||
assertThat(vo.getNickname()).isEqualTo("测试会员");
|
||||
})
|
||||
.verifyComplete();
|
||||
|
||||
verify(redisUtil).get(CACHE_INFO_KEY, MemberInfoVO.class);
|
||||
verifyNoInteractions(memberRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("缓存未命中时应从数据库查询并更新缓存")
|
||||
void shouldQueryDatabaseAndCacheWhenCacheMiss() {
|
||||
when(redisUtil.get(CACHE_INFO_KEY, MemberInfoVO.class)).thenReturn(Mono.empty());
|
||||
Member member = createTestMember();
|
||||
when(memberRepository.findById(MEMBER_ID)).thenReturn(Mono.just(member));
|
||||
when(redisUtil.setWithExpire(eq(CACHE_INFO_KEY), any(MemberInfoVO.class), anyLong()))
|
||||
.thenReturn(Mono.just(true));
|
||||
|
||||
try (MockedStatic<AesUtil> aesUtilMock = mockStatic(AesUtil.class)) {
|
||||
aesUtilMock.when(() -> AesUtil.decrypt(ENCRYPTED_PHONE)).thenReturn(DECRYPTED_PHONE);
|
||||
|
||||
Mono<MemberInfoVO> result = memberServiceImpl.getMemberInfo(MEMBER_ID);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(vo -> {
|
||||
assertThat(vo.getId()).isEqualTo(MEMBER_ID);
|
||||
assertThat(vo.getPhone()).isEqualTo(MASKED_PHONE);
|
||||
assertThat(vo.getGender()).isEqualTo(GenderEnum.MALE);
|
||||
assertThat(vo.getHasPhone()).isTrue();
|
||||
})
|
||||
.verifyComplete();
|
||||
|
||||
verify(memberRepository).findById(MEMBER_ID);
|
||||
verify(redisUtil).setWithExpire(eq(CACHE_INFO_KEY), any(MemberInfoVO.class), eq(300L));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("会员不存在时应抛出 NotFoundException")
|
||||
void shouldThrowNotFoundExceptionWhenMemberNotFound() {
|
||||
when(redisUtil.get(CACHE_INFO_KEY, MemberInfoVO.class)).thenReturn(Mono.empty());
|
||||
when(memberRepository.findById(MEMBER_ID)).thenReturn(Mono.empty());
|
||||
|
||||
Mono<MemberInfoVO> result = memberServiceImpl.getMemberInfo(MEMBER_ID);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectErrorMatches(throwable ->
|
||||
throwable instanceof NotFoundException
|
||||
&& ((NotFoundException) throwable).getErrorCode().equals(ErrorCode.NOT_FOUND_USER)
|
||||
&& throwable.getMessage().equals("会员不存在"))
|
||||
.verify();
|
||||
|
||||
verify(redisUtil).get(CACHE_INFO_KEY, MemberInfoVO.class);
|
||||
verify(memberRepository).findById(MEMBER_ID);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== updateMemberInfo ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("updateMemberInfo 方法")
|
||||
class UpdateMemberInfoTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("成功更新会员信息时应返回更新后的 MemberInfoVO")
|
||||
void shouldReturnUpdatedInfoWhenSuccess() {
|
||||
Member member = createTestMember();
|
||||
UpdateMemberInfoDto dto = createUpdateDto();
|
||||
|
||||
when(memberRepository.findById(MEMBER_ID)).thenReturn(Mono.just(member));
|
||||
when(memberRepository.save(any(Member.class))).thenAnswer(invocation -> Mono.just(invocation.getArgument(0)));
|
||||
when(redisUtil.delete("member:info:" + MEMBER_ID)).thenReturn(Mono.just(1L));
|
||||
when(redisUtil.delete("member:detail:" + MEMBER_ID)).thenReturn(Mono.just(1L));
|
||||
|
||||
try (MockedStatic<AesUtil> aesUtilMock = mockStatic(AesUtil.class)) {
|
||||
aesUtilMock.when(() -> AesUtil.decrypt(ENCRYPTED_PHONE)).thenReturn(DECRYPTED_PHONE);
|
||||
|
||||
Mono<MemberInfoVO> result = memberServiceImpl.updateMemberInfo(MEMBER_ID, dto);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(vo -> {
|
||||
assertThat(vo.getId()).isEqualTo(MEMBER_ID);
|
||||
assertThat(vo.getNickname()).isEqualTo(HtmlEscapeUtil.escape("新昵称"));
|
||||
assertThat(vo.getGender()).isEqualTo(GenderEnum.FEMALE);
|
||||
})
|
||||
.verifyComplete();
|
||||
|
||||
verify(memberRepository).findById(MEMBER_ID);
|
||||
verify(memberRepository).save(any(Member.class));
|
||||
verify(mockSyncer).sync(any(Member.class));
|
||||
verify(redisUtil).delete("member:info:" + MEMBER_ID);
|
||||
verify(redisUtil).delete("member:detail:" + MEMBER_ID);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("会员不存在时应抛出 NotFoundException")
|
||||
void shouldThrowNotFoundExceptionWhenMemberNotFound() {
|
||||
UpdateMemberInfoDto dto = createUpdateDto();
|
||||
when(memberRepository.findById(MEMBER_ID)).thenReturn(Mono.empty());
|
||||
|
||||
Mono<MemberInfoVO> result = memberServiceImpl.updateMemberInfo(MEMBER_ID, dto);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectErrorMatches(throwable ->
|
||||
throwable instanceof NotFoundException
|
||||
&& throwable.getMessage().equals("会员不存在"))
|
||||
.verify();
|
||||
|
||||
verify(memberRepository).findById(MEMBER_ID);
|
||||
verifyNoMoreInteractions(memberRepository);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== adminUpdatePhone ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("adminUpdatePhone 方法")
|
||||
class AdminUpdatePhoneTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("手机号未被占用时应成功绑定手机号")
|
||||
void shouldBindPhoneWhenPhoneNotOccupied() {
|
||||
Member member = createTestMember();
|
||||
member.setPhone(null);
|
||||
|
||||
when(memberRepository.findById(MEMBER_ID)).thenReturn(Mono.just(member));
|
||||
when(memberRepository.save(any(Member.class))).thenAnswer(invocation -> Mono.just(invocation.getArgument(0)));
|
||||
when(redisUtil.delete("member:info:" + MEMBER_ID)).thenReturn(Mono.just(1L));
|
||||
when(redisUtil.delete("member:detail:" + MEMBER_ID)).thenReturn(Mono.just(1L));
|
||||
|
||||
try (MockedStatic<AesUtil> aesUtilMock = mockStatic(AesUtil.class)) {
|
||||
aesUtilMock.when(() -> AesUtil.encrypt(TEST_PHONE)).thenReturn(ENCRYPTED_PHONE);
|
||||
when(memberRepository.findByPhone(ENCRYPTED_PHONE)).thenReturn(Mono.empty());
|
||||
|
||||
Mono<Boolean> result = memberServiceImpl.adminUpdatePhone(MEMBER_ID, TEST_PHONE);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(success -> assertThat(success).isTrue())
|
||||
.verifyComplete();
|
||||
|
||||
verify(memberRepository).findByPhone(ENCRYPTED_PHONE);
|
||||
verify(memberRepository).findById(MEMBER_ID);
|
||||
verify(memberRepository).save(any(Member.class));
|
||||
verify(mockSyncer).sync(any(Member.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("手机号已被自己绑定时应抛出 ConflictException")
|
||||
void shouldThrowConflictWhenPhoneBoundToSelf() {
|
||||
Member existingMember = createTestMember();
|
||||
|
||||
try (MockedStatic<AesUtil> aesUtilMock = mockStatic(AesUtil.class)) {
|
||||
aesUtilMock.when(() -> AesUtil.encrypt(TEST_PHONE)).thenReturn(ENCRYPTED_PHONE);
|
||||
when(memberRepository.findByPhone(ENCRYPTED_PHONE)).thenReturn(Mono.just(existingMember));
|
||||
|
||||
Mono<Boolean> result = memberServiceImpl.adminUpdatePhone(MEMBER_ID, TEST_PHONE);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectErrorMatches(throwable ->
|
||||
throwable instanceof ConflictException
|
||||
&& throwable.getMessage().equals("重复绑定"))
|
||||
.verify();
|
||||
|
||||
verify(memberRepository).findByPhone(ENCRYPTED_PHONE);
|
||||
verifyNoMoreInteractions(memberRepository);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("手机号已被他人绑定时应抛出 ConflictException")
|
||||
void shouldThrowConflictWhenPhoneBoundToOther() {
|
||||
Member otherMember = createTestMember();
|
||||
otherMember.setId(99999L);
|
||||
|
||||
try (MockedStatic<AesUtil> aesUtilMock = mockStatic(AesUtil.class)) {
|
||||
aesUtilMock.when(() -> AesUtil.encrypt(TEST_PHONE)).thenReturn(ENCRYPTED_PHONE);
|
||||
when(memberRepository.findByPhone(ENCRYPTED_PHONE)).thenReturn(Mono.just(otherMember));
|
||||
|
||||
Mono<Boolean> result = memberServiceImpl.adminUpdatePhone(MEMBER_ID, TEST_PHONE);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectErrorMatches(throwable ->
|
||||
throwable instanceof ConflictException
|
||||
&& throwable.getMessage().equals("该手机号已被其他会员绑定"))
|
||||
.verify();
|
||||
|
||||
verify(memberRepository).findByPhone(ENCRYPTED_PHONE);
|
||||
verifyNoMoreInteractions(memberRepository);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== searchMember ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("searchMember 方法")
|
||||
class SearchMemberTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("搜索值为手机号格式时应加密后搜索 ES")
|
||||
void shouldEncryptAndSearchESWhenPhoneFormat() {
|
||||
SearchMemberDto dto = new SearchMemberDto("13812345678", 1, 10);
|
||||
|
||||
MemberES memberES = new MemberES();
|
||||
memberES.setId(String.valueOf(MEMBER_ID));
|
||||
memberES.setMemberNo("M20260001");
|
||||
memberES.setNickname("测试会员");
|
||||
|
||||
try (MockedStatic<AesUtil> aesUtilMock = mockStatic(AesUtil.class)) {
|
||||
aesUtilMock.when(() -> AesUtil.encrypt("13812345678")).thenReturn(ENCRYPTED_PHONE);
|
||||
when(memberESRepository.findByMemberNoOrPhoneOrNicknameContaining(
|
||||
eq(ENCRYPTED_PHONE), eq(ENCRYPTED_PHONE), eq(ENCRYPTED_PHONE), any(Pageable.class)))
|
||||
.thenReturn(Flux.just(memberES));
|
||||
|
||||
Flux<MemberES> result = memberServiceImpl.searchMember(dto);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(es -> {
|
||||
assertThat(es.getId()).isEqualTo(String.valueOf(MEMBER_ID));
|
||||
assertThat(es.getNickname()).isEqualTo("测试会员");
|
||||
})
|
||||
.verifyComplete();
|
||||
|
||||
aesUtilMock.verify(() -> AesUtil.encrypt("13812345678"));
|
||||
verify(memberESRepository).findByMemberNoOrPhoneOrNicknameContaining(
|
||||
eq(ENCRYPTED_PHONE), eq(ENCRYPTED_PHONE), eq(ENCRYPTED_PHONE), any(Pageable.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("搜索值为关键词时应直接搜索 ES")
|
||||
void shouldSearchESDirectlyWithKeyword() {
|
||||
String keyword = "张三";
|
||||
SearchMemberDto dto = new SearchMemberDto(keyword, 1, 10);
|
||||
|
||||
MemberES memberES = new MemberES();
|
||||
memberES.setId("20001");
|
||||
memberES.setNickname("张三");
|
||||
|
||||
when(memberESRepository.findByMemberNoOrPhoneOrNicknameContaining(
|
||||
eq(keyword), eq(keyword), eq(keyword), any(Pageable.class)))
|
||||
.thenReturn(Flux.just(memberES));
|
||||
|
||||
Flux<MemberES> result = memberServiceImpl.searchMember(dto);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(es -> assertThat(es.getNickname()).isEqualTo("张三"))
|
||||
.verifyComplete();
|
||||
|
||||
verify(memberESRepository).findByMemberNoOrPhoneOrNicknameContaining(
|
||||
eq(keyword), eq(keyword), eq(keyword), any(Pageable.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("搜索值为空时应返回空结果")
|
||||
void shouldReturnEmptyWhenSearchValueIsNull() {
|
||||
SearchMemberDto dto = new SearchMemberDto(null, 1, 10);
|
||||
|
||||
Flux<MemberES> result = memberServiceImpl.searchMember(dto);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.verifyComplete();
|
||||
|
||||
verifyNoInteractions(memberESRepository);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== findAll ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("findAll 方法")
|
||||
class FindAllTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("合法的排序字段应使用指定排序")
|
||||
void shouldUseSpecifiedSortFieldWhenAllowed() {
|
||||
Member member = createTestMember();
|
||||
when(memberRepository.findAllBy(any(Pageable.class))).thenReturn(Flux.just(member));
|
||||
|
||||
Flux<Member> result = memberServiceImpl.findAll(1, 10, "createdAt", "asc");
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(m -> assertThat(m.getId()).isEqualTo(MEMBER_ID))
|
||||
.verifyComplete();
|
||||
|
||||
verify(memberRepository).findAllBy(argThat(pageable -> {
|
||||
var sort = pageable.getSort();
|
||||
return sort.iterator().next().getProperty().equals("created_at")
|
||||
&& sort.iterator().next().isAscending();
|
||||
}));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("排序字段为 null 时应使用默认排序")
|
||||
void shouldUseDefaultSortWhenSortFieldIsNull() {
|
||||
Member member = createTestMember();
|
||||
when(memberRepository.findAllBy(any(Pageable.class))).thenReturn(Flux.just(member));
|
||||
|
||||
Flux<Member> result = memberServiceImpl.findAll(1, 10, null, null);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(m -> assertThat(m.getId()).isEqualTo(MEMBER_ID))
|
||||
.verifyComplete();
|
||||
|
||||
verify(memberRepository).findAllBy(argThat(pageable -> {
|
||||
var sort = pageable.getSort();
|
||||
return sort.iterator().next().getProperty().equals("created_at")
|
||||
&& sort.iterator().next().isDescending();
|
||||
}));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("白名单外的排序字段应使用默认排序")
|
||||
void shouldUseDefaultSortWhenSortFieldNotInWhitelist() {
|
||||
Member member = createTestMember();
|
||||
when(memberRepository.findAllBy(any(Pageable.class))).thenReturn(Flux.just(member));
|
||||
|
||||
Flux<Member> result = memberServiceImpl.findAll(1, 10, "invalidField", "desc");
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(m -> assertThat(m.getId()).isEqualTo(MEMBER_ID))
|
||||
.verifyComplete();
|
||||
|
||||
verify(memberRepository).findAllBy(argThat(pageable -> {
|
||||
var sort = pageable.getSort();
|
||||
return sort.iterator().next().getProperty().equals("created_at");
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== getMemberDetail ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("getMemberDetail 方法")
|
||||
class GetMemberDetailTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("缓存命中时应直接返回缓存数据")
|
||||
void shouldReturnCachedDataWhenCacheHit() {
|
||||
MemberDetailVO cachedDetail = MemberDetailVO.builder()
|
||||
.id(MEMBER_ID)
|
||||
.memberNo("M20260001")
|
||||
.nickname("测试会员")
|
||||
.build();
|
||||
|
||||
when(redisUtil.get(CACHE_DETAIL_KEY, MemberDetailVO.class)).thenReturn(Mono.just(cachedDetail));
|
||||
|
||||
Mono<MemberDetailVO> result = memberServiceImpl.getMemberDetail(MEMBER_ID);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(detail -> {
|
||||
assertThat(detail.getId()).isEqualTo(MEMBER_ID);
|
||||
assertThat(detail.getNickname()).isEqualTo("测试会员");
|
||||
})
|
||||
.verifyComplete();
|
||||
|
||||
verify(redisUtil).get(CACHE_DETAIL_KEY, MemberDetailVO.class);
|
||||
verifyNoInteractions(memberRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("缓存未命中时应从数据库查询并构建详情")
|
||||
void shouldQueryDatabaseAndBuildDetailWhenCacheMiss() {
|
||||
when(redisUtil.get(CACHE_DETAIL_KEY, MemberDetailVO.class)).thenReturn(Mono.empty());
|
||||
|
||||
Member member = createTestMember();
|
||||
when(memberRepository.findById(MEMBER_ID)).thenReturn(Mono.just(member));
|
||||
|
||||
MemberCardInfoVO cardInfo = createTestCardInfoVO();
|
||||
when(memberRepository.findCardRecordsWithCardInfoByMemberId(MEMBER_ID))
|
||||
.thenReturn(Flux.just(cardInfo));
|
||||
|
||||
when(redisUtil.setWithExpire(eq(CACHE_DETAIL_KEY), any(MemberDetailVO.class), anyLong()))
|
||||
.thenReturn(Mono.just(true));
|
||||
|
||||
Mono<MemberDetailVO> result = memberServiceImpl.getMemberDetail(MEMBER_ID);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(detail -> {
|
||||
assertThat(detail.getId()).isEqualTo(MEMBER_ID);
|
||||
assertThat(detail.getMemberNo()).isEqualTo("M20260001");
|
||||
assertThat(detail.getNickname()).isEqualTo("测试会员");
|
||||
assertThat(detail.getGenderDesc()).isEqualTo("男");
|
||||
assertThat(detail.getMemberCards()).hasSize(1);
|
||||
assertThat(detail.getActiveCardCount()).isEqualTo(1);
|
||||
assertThat(detail.getInactiveCardCount()).isEqualTo(0);
|
||||
})
|
||||
.verifyComplete();
|
||||
|
||||
verify(memberRepository).findById(MEMBER_ID);
|
||||
verify(memberRepository).findCardRecordsWithCardInfoByMemberId(MEMBER_ID);
|
||||
verify(redisUtil).setWithExpire(eq(CACHE_DETAIL_KEY), any(MemberDetailVO.class), eq(300L));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("会员不存在时应抛出 NotFoundException")
|
||||
void shouldThrowNotFoundExceptionWhenMemberNotFound() {
|
||||
when(redisUtil.get(CACHE_DETAIL_KEY, MemberDetailVO.class)).thenReturn(Mono.empty());
|
||||
when(memberRepository.findById(MEMBER_ID)).thenReturn(Mono.empty());
|
||||
// zipWith 会订阅所有源,即使另一个是 error,也需要桩避免 NPE
|
||||
lenient().when(memberRepository.findCardRecordsWithCardInfoByMemberId(MEMBER_ID))
|
||||
.thenReturn(Flux.empty());
|
||||
|
||||
Mono<MemberDetailVO> result = memberServiceImpl.getMemberDetail(MEMBER_ID);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectErrorMatches(throwable ->
|
||||
throwable instanceof NotFoundException
|
||||
&& throwable.getMessage().equals("会员不存在"))
|
||||
.verify();
|
||||
|
||||
verify(memberRepository).findById(MEMBER_ID);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== adminUpdateMemberInfo ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("adminUpdateMemberInfo 方法")
|
||||
class AdminUpdateMemberInfoTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("成功更新会员信息时应返回 true")
|
||||
void shouldReturnTrueWhenSuccess() {
|
||||
Member member = createTestMember();
|
||||
UpdateMemberInfoDto dto = createUpdateDto();
|
||||
|
||||
when(memberRepository.findById(MEMBER_ID)).thenReturn(Mono.just(member));
|
||||
when(memberRepository.save(any(Member.class))).thenAnswer(invocation -> Mono.just(invocation.getArgument(0)));
|
||||
when(redisUtil.delete("member:info:" + MEMBER_ID)).thenReturn(Mono.just(1L));
|
||||
when(redisUtil.delete("member:detail:" + MEMBER_ID)).thenReturn(Mono.just(1L));
|
||||
|
||||
Mono<Boolean> result = memberServiceImpl.adminUpdateMemberInfo(MEMBER_ID, dto);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(success -> assertThat(success).isTrue())
|
||||
.verifyComplete();
|
||||
|
||||
verify(memberRepository).findById(MEMBER_ID);
|
||||
verify(memberRepository).save(any(Member.class));
|
||||
verify(mockSyncer).sync(any(Member.class));
|
||||
verify(redisUtil).delete("member:info:" + MEMBER_ID);
|
||||
verify(redisUtil).delete("member:detail:" + MEMBER_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("保存异常时应返回 false")
|
||||
void shouldReturnFalseWhenExceptionOccurs() {
|
||||
Member member = createTestMember();
|
||||
UpdateMemberInfoDto dto = createUpdateDto();
|
||||
|
||||
when(memberRepository.findById(MEMBER_ID)).thenReturn(Mono.just(member));
|
||||
when(memberRepository.save(any(Member.class)))
|
||||
.thenReturn(Mono.error(new RuntimeException("数据库异常")));
|
||||
|
||||
Mono<Boolean> result = memberServiceImpl.adminUpdateMemberInfo(MEMBER_ID, dto);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(success -> assertThat(success).isFalse())
|
||||
.verifyComplete();
|
||||
|
||||
verify(memberRepository).findById(MEMBER_ID);
|
||||
verify(memberRepository).save(any(Member.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("会员不存在时应返回 false")
|
||||
void shouldReturnFalseWhenMemberNotFound() {
|
||||
UpdateMemberInfoDto dto = createUpdateDto();
|
||||
|
||||
when(memberRepository.findById(MEMBER_ID))
|
||||
.thenReturn(Mono.error(new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在")));
|
||||
|
||||
Mono<Boolean> result = memberServiceImpl.adminUpdateMemberInfo(MEMBER_ID, dto);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.assertNext(success -> assertThat(success).isFalse())
|
||||
.verifyComplete();
|
||||
|
||||
verify(memberRepository).findById(MEMBER_ID);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user