完成自动化测试套件实施(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:
2026-08-02 08:28:37 +08:00
parent dc68581c5e
commit 015cb0dc78
119 changed files with 21873 additions and 553 deletions
+5
View File
@@ -61,6 +61,11 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations-jakarta</artifactId>
@@ -156,6 +156,7 @@ public class GroupCourseHandler {
response.put("data", course);
return ServerResponse.ok().bodyValue(response);
})
.switchIfEmpty(ServerResponse.notFound().build())
.onErrorResume(error -> {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
@@ -0,0 +1,256 @@
package cn.novalon.gym.manage.groupcourse.handler;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseBooking;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseBookingRepository;
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.enums.MemberCardType;
import cn.novalon.gym.manage.member.repository.MemberCardRepository;
import cn.novalon.gym.manage.member.service.IMemberCardRecordService;
import cn.novalon.gym.manage.member.service.IMemberStoredCardService;
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 reactor.core.publisher.Mono;
import java.math.BigDecimal;
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)
class BookingSagaHandlerTest {
@Mock
private IGroupCourseBookingRepository bookingRepository;
@Mock
private IMemberCardRecordService memberCardRecordService;
@Mock
private IMemberStoredCardService memberStoredCardService;
@Mock
private MemberCardRepository memberCardRepository;
private BookingSagaHandler handler;
@BeforeEach
void setUp() {
handler = new BookingSagaHandler(bookingRepository, memberCardRecordService,
memberStoredCardService, memberCardRepository);
}
// ==================== executeBooking ====================
@Test
void executeBooking_shouldSucceedForStoredValueCard() {
GroupCourseBooking booking = createTestBooking(1L, 100L, 10L);
MemberCardRecord record = createTestMemberCardRecord(50L, 100L, 1L);
MemberCard card = createTestMemberCard(1L, MemberCardType.STORED_VALUE_CARD.name());
when(bookingRepository.save(booking)).thenReturn(Mono.just(booking));
when(memberCardRecordService.findById(50L)).thenReturn(Mono.just(record));
when(memberCardRepository.findById(1L)).thenReturn(Mono.just(card));
when(memberStoredCardService.consume(100L, BigDecimal.valueOf(50.0))).thenReturn(Mono.just(1));
Mono<GroupCourseBooking> result = handler.executeBooking(booking, 50L, BigDecimal.valueOf(50.0));
GroupCourseBooking savedBooking = result.block();
assertThat(savedBooking).isNotNull();
assertThat(savedBooking.getId()).isEqualTo(1L);
verify(bookingRepository).save(booking);
verify(memberCardRecordService).findById(50L);
verify(memberCardRepository).findById(1L);
verify(memberStoredCardService).consume(100L, BigDecimal.valueOf(50.0));
}
@Test
void executeBooking_shouldFailWhenRecordNotFound() {
GroupCourseBooking booking = createTestBooking(1L, 100L, 10L);
when(bookingRepository.save(booking)).thenReturn(Mono.just(booking));
when(memberCardRecordService.findById(99L)).thenReturn(Mono.empty());
Mono<GroupCourseBooking> result = handler.executeBooking(booking, 99L, BigDecimal.valueOf(50.0));
try {
result.block();
} catch (Exception e) {
assertThat(e.getMessage()).contains("会员卡记录不存在");
}
verify(bookingRepository).save(booking);
// Saga rollback 由于 error handler 传播链会调用多次 deleteBooking
verify(bookingRepository, atLeast(1)).deleteById(1L);
}
@Test
void executeBooking_shouldFailWhenCardNotBelongToMember() {
GroupCourseBooking booking = createTestBooking(1L, 100L, 10L);
MemberCardRecord record = createTestMemberCardRecord(50L, 999L, 1L); // memberId mismatch
when(bookingRepository.save(booking)).thenReturn(Mono.just(booking));
when(memberCardRecordService.findById(50L)).thenReturn(Mono.just(record));
Mono<GroupCourseBooking> result = handler.executeBooking(booking, 50L, BigDecimal.valueOf(50.0));
try {
result.block();
} catch (Exception e) {
assertThat(e.getMessage()).contains("会员卡不归属当前用户");
}
verify(bookingRepository).save(booking);
verify(bookingRepository, atLeast(1)).deleteById(1L);
}
@Test
void executeBooking_shouldFailForCountCard() {
GroupCourseBooking booking = createTestBooking(1L, 100L, 10L);
MemberCardRecord record = createTestMemberCardRecord(50L, 100L, 1L);
MemberCard card = createTestMemberCard(1L, MemberCardType.COUNT_CARD.name());
when(bookingRepository.save(booking)).thenReturn(Mono.just(booking));
when(memberCardRecordService.findById(50L)).thenReturn(Mono.just(record));
when(memberCardRepository.findById(1L)).thenReturn(Mono.just(card));
Mono<GroupCourseBooking> result = handler.executeBooking(booking, 50L, BigDecimal.valueOf(50.0));
try {
result.block();
} catch (Exception e) {
assertThat(e.getMessage()).contains("团课预约仅支持储值卡和时长卡支付");
}
verify(bookingRepository).save(booking);
verify(bookingRepository, atLeast(1)).deleteById(1L);
}
@Test
void executeBooking_shouldSucceedForTimeCard() {
GroupCourseBooking booking = createTestBooking(1L, 100L, 10L);
MemberCardRecord record = createTestMemberCardRecord(50L, 100L, 1L);
record.setExpireTime(LocalDateTime.now().plusDays(30));
record.setStatus(MemberCardRecordStatus.ACTIVE);
MemberCard card = createTestMemberCard(1L, MemberCardType.TIME_CARD.name());
when(bookingRepository.save(booking)).thenReturn(Mono.just(booking));
when(memberCardRecordService.findById(50L)).thenReturn(Mono.just(record));
when(memberCardRepository.findById(1L)).thenReturn(Mono.just(card));
when(memberStoredCardService.consume(100L, BigDecimal.valueOf(50.0))).thenReturn(Mono.just(1));
Mono<GroupCourseBooking> result = handler.executeBooking(booking, 50L, BigDecimal.valueOf(50.0));
GroupCourseBooking savedBooking = result.block();
assertThat(savedBooking).isNotNull();
verify(bookingRepository).save(booking);
verify(memberCardRecordService).findById(50L);
verify(memberCardRepository).findById(1L);
verify(memberStoredCardService).consume(100L, BigDecimal.valueOf(50.0));
}
@Test
void executeBooking_shouldFailWhenTimeCardExpired() {
GroupCourseBooking booking = createTestBooking(1L, 100L, 10L);
MemberCardRecord record = createTestMemberCardRecord(50L, 100L, 1L);
record.setExpireTime(LocalDateTime.now().minusDays(1));
record.setStatus(MemberCardRecordStatus.ACTIVE);
MemberCard card = createTestMemberCard(1L, MemberCardType.TIME_CARD.name());
when(bookingRepository.save(booking)).thenReturn(Mono.just(booking));
when(memberCardRecordService.findById(50L)).thenReturn(Mono.just(record));
when(memberCardRepository.findById(1L)).thenReturn(Mono.just(card));
// 需要 mock consume 避免 Saga 构造时 NPE
when(memberStoredCardService.consume(anyLong(), any())).thenReturn(Mono.just(1));
Mono<GroupCourseBooking> result = handler.executeBooking(booking, 50L, BigDecimal.valueOf(50.0));
try {
result.block();
} catch (Exception e) {
assertThat(e.getMessage()).contains("已过期");
}
verify(bookingRepository).save(booking);
verify(bookingRepository, atLeast(1)).deleteById(1L);
}
// ==================== executeCancelBooking ====================
@Test
void executeCancelBooking_shouldSucceed() {
GroupCourseBooking booking = createTestBooking(1L, 100L, 10L);
booking.setStatus("1");
when(bookingRepository.updateStatus(1L, "1")).thenReturn(Mono.just(1));
when(memberStoredCardService.refundBalanceWithFee(100L, BigDecimal.valueOf(50.0), 1L))
.thenReturn(Mono.just(BigDecimal.valueOf(50.0)));
when(bookingRepository.findById(1L)).thenReturn(Mono.just(booking));
Mono<GroupCourseBooking> result = handler.executeCancelBooking(1L, 10L, 50L, 100L, BigDecimal.valueOf(50.0), 1L);
GroupCourseBooking cancelledBooking = result.block();
assertThat(cancelledBooking).isNotNull();
assertThat(cancelledBooking.getStatus()).isEqualTo("1");
verify(bookingRepository).updateStatus(1L, "1");
verify(memberStoredCardService).refundBalanceWithFee(100L, BigDecimal.valueOf(50.0), 1L);
verify(bookingRepository).findById(1L);
}
@Test
void executeCancelBooking_shouldFailWhenUpdateStatusFails() {
when(bookingRepository.updateStatus(1L, "1")).thenReturn(Mono.just(0));
when(bookingRepository.findById(1L)).thenReturn(Mono.just(createTestBooking(1L, 100L, 10L)));
// 需要 mock refundBalanceWithFee 避免 SagaStep 构造时 NPE
when(memberStoredCardService.refundBalanceWithFee(anyLong(), any(), anyLong()))
.thenReturn(Mono.just(BigDecimal.ZERO));
Mono<GroupCourseBooking> result = handler.executeCancelBooking(1L, 10L, 50L, 100L, BigDecimal.valueOf(50.0), 1L);
try {
result.block();
} catch (Exception e) {
assertThat(e.getMessage()).contains("更新预约状态失败");
}
verify(bookingRepository).updateStatus(1L, "1");
}
// ==================== helper ====================
private GroupCourseBooking createTestBooking(Long id, Long memberId, Long courseId) {
GroupCourseBooking booking = new GroupCourseBooking();
booking.setId(id);
booking.setMemberId(memberId);
booking.setCourseId(courseId);
booking.setStatus("0");
return booking;
}
private MemberCardRecord createTestMemberCardRecord(Long id, Long memberId, Long memberCardId) {
MemberCardRecord record = new MemberCardRecord();
record.setId(id);
record.setMemberId(memberId);
record.setMemberCardId(memberCardId);
record.setStatus(MemberCardRecordStatus.ACTIVE);
record.setExpireTime(LocalDateTime.now().plusDays(30));
return record;
}
private MemberCard createTestMemberCard(Long memberCardId, String cardType) {
return MemberCard.builder()
.memberCardId(memberCardId)
.memberCardType(cardType)
.memberCardName("测试卡")
.build();
}
}
@@ -0,0 +1,471 @@
package cn.novalon.gym.manage.groupcourse.handler;
import cn.novalon.gym.manage.common.dto.PageRequest;
import cn.novalon.gym.manage.common.dto.PageResponse;
import cn.novalon.gym.manage.groupcourse.domain.CourseLabel;
import cn.novalon.gym.manage.groupcourse.service.ICourseLabelService;
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.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class CourseLabelHandlerTest {
@Mock
private ICourseLabelService courseLabelService;
private CourseLabelHandler handler;
@BeforeEach
void setUp() {
handler = new CourseLabelHandler(courseLabelService);
}
// ==================== getAllLabels ====================
@Test
void getAllLabels_shouldReturnOk() {
CourseLabel label1 = new CourseLabel();
label1.setId(1L);
label1.setLabelName("适合新手");
CourseLabel label2 = new CourseLabel();
label2.setId(2L);
label2.setLabelName("高强度");
when(courseLabelService.findAll()).thenReturn(Flux.just(label1, label2));
MockServerRequest request = MockServerRequest.builder().build();
Mono<ServerResponse> result = handler.getAllLabels(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(courseLabelService).findAll();
}
@Test
void getAllLabels_shouldReturnOkWhenEmpty() {
when(courseLabelService.findAll()).thenReturn(Flux.empty());
MockServerRequest request = MockServerRequest.builder().build();
Mono<ServerResponse> result = handler.getAllLabels(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
}
// ==================== getLabelsByPage ====================
@Test
void getLabelsByPage_shouldReturnOk() {
PageRequest pageRequest = new PageRequest();
PageResponse<CourseLabel> pageResponse = new PageResponse<>();
when(courseLabelService.findByPage(any(PageRequest.class))).thenReturn(Mono.just(pageResponse));
MockServerRequest request = MockServerRequest.builder()
.body(Mono.just(pageRequest));
Mono<ServerResponse> result = handler.getLabelsByPage(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(courseLabelService).findByPage(any(PageRequest.class));
}
// ==================== getLabelById ====================
@Test
void getLabelById_shouldReturnOkWhenFound() {
CourseLabel label = new CourseLabel();
label.setId(1L);
label.setLabelName("适合新手");
when(courseLabelService.findById(1L)).thenReturn(Mono.just(label));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("id", "1")
.build();
Mono<ServerResponse> result = handler.getLabelById(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(courseLabelService).findById(1L);
}
@Test
void getLabelById_shouldReturnNotFound() {
when(courseLabelService.findById(999L)).thenReturn(Mono.empty());
MockServerRequest request = MockServerRequest.builder()
.pathVariable("id", "999")
.build();
Mono<ServerResponse> result = handler.getLabelById(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
// ==================== searchLabels ====================
@Test
void searchLabels_shouldReturnOk() {
CourseLabel label = new CourseLabel();
label.setId(1L);
label.setLabelName("新手");
when(courseLabelService.findByKeyword("新手")).thenReturn(Flux.just(label));
MockServerRequest request = MockServerRequest.builder()
.queryParam("keyword", "新手")
.build();
Mono<ServerResponse> result = handler.searchLabels(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(courseLabelService).findByKeyword("新手");
}
@Test
void searchLabels_shouldReturnOkWhenEmpty() {
when(courseLabelService.findByKeyword("")).thenReturn(Flux.empty());
MockServerRequest request = MockServerRequest.builder().build();
Mono<ServerResponse> result = handler.searchLabels(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
}
// ==================== createLabel ====================
@Test
void createLabel_shouldReturnOk() {
CourseLabel label = new CourseLabel();
label.setLabelName("适合新手");
label.setColor("#52c41a");
CourseLabel created = new CourseLabel();
created.setId(1L);
created.setLabelName("适合新手");
created.setColor("#52c41a");
when(courseLabelService.create(any(CourseLabel.class))).thenReturn(Mono.just(created));
MockServerRequest request = MockServerRequest.builder()
.body(Mono.just(label));
Mono<ServerResponse> result = handler.createLabel(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(courseLabelService).create(any(CourseLabel.class));
}
@Test
void createLabel_shouldReturn400WhenNameEmpty() {
CourseLabel label = new CourseLabel();
label.setLabelName("");
label.setColor("#52c41a");
MockServerRequest request = MockServerRequest.builder()
.body(Mono.just(label));
Mono<ServerResponse> result = handler.createLabel(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
verify(courseLabelService, never()).create(any());
}
@Test
void createLabel_shouldReturn400WhenNameNull() {
CourseLabel label = new CourseLabel();
label.setColor("#52c41a");
MockServerRequest request = MockServerRequest.builder()
.body(Mono.just(label));
Mono<ServerResponse> result = handler.createLabel(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
verify(courseLabelService, never()).create(any());
}
@Test
void createLabel_shouldReturn400WhenNameTooLong() {
CourseLabel label = new CourseLabel();
label.setLabelName("a".repeat(51));
MockServerRequest request = MockServerRequest.builder()
.body(Mono.just(label));
Mono<ServerResponse> result = handler.createLabel(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
verify(courseLabelService, never()).create(any());
}
@Test
void createLabel_shouldSetDefaultColorWhenColorEmpty() {
CourseLabel label = new CourseLabel();
label.setLabelName("适合新手");
CourseLabel created = new CourseLabel();
created.setId(1L);
created.setLabelName("适合新手");
created.setColor("#1890ff");
when(courseLabelService.create(any(CourseLabel.class))).thenReturn(Mono.just(created));
MockServerRequest request = MockServerRequest.builder()
.body(Mono.just(label));
Mono<ServerResponse> result = handler.createLabel(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
}
@Test
void createLabel_shouldReturn400WhenServiceError() {
CourseLabel label = new CourseLabel();
label.setLabelName("适合新手");
when(courseLabelService.create(any(CourseLabel.class)))
.thenReturn(Mono.error(new RuntimeException("标签已存在")));
MockServerRequest request = MockServerRequest.builder()
.body(Mono.just(label));
Mono<ServerResponse> result = handler.createLabel(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
// ==================== updateLabel ====================
@Test
void updateLabel_shouldReturnOk() {
CourseLabel label = new CourseLabel();
label.setLabelName("适合进阶");
CourseLabel updated = new CourseLabel();
updated.setId(1L);
updated.setLabelName("适合进阶");
when(courseLabelService.update(eq(1L), any(CourseLabel.class))).thenReturn(Mono.just(updated));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("id", "1")
.body(Mono.just(label));
Mono<ServerResponse> result = handler.updateLabel(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(courseLabelService).update(eq(1L), any(CourseLabel.class));
}
@Test
void updateLabel_shouldReturn400WhenNameTooLong() {
CourseLabel label = new CourseLabel();
label.setLabelName("a".repeat(51));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("id", "1")
.body(Mono.just(label));
Mono<ServerResponse> result = handler.updateLabel(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
verify(courseLabelService, never()).update(anyLong(), any());
}
@Test
void updateLabel_shouldReturn400WhenServiceError() {
CourseLabel label = new CourseLabel();
label.setLabelName("适合进阶");
when(courseLabelService.update(eq(1L), any(CourseLabel.class)))
.thenReturn(Mono.error(new RuntimeException("标签不存在")));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("id", "1")
.body(Mono.just(label));
Mono<ServerResponse> result = handler.updateLabel(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
// ==================== deleteLabel ====================
@Test
void deleteLabel_shouldReturnOk() {
when(courseLabelService.delete(1L)).thenReturn(Mono.empty());
MockServerRequest request = MockServerRequest.builder()
.pathVariable("id", "1")
.build();
Mono<ServerResponse> result = handler.deleteLabel(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(courseLabelService).delete(1L);
}
@Test
void deleteLabel_shouldReturn400WhenServiceError() {
when(courseLabelService.delete(1L)).thenReturn(Mono.error(new RuntimeException("删除失败")));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("id", "1")
.build();
Mono<ServerResponse> result = handler.deleteLabel(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
// ==================== getLabelsByTypeId ====================
@Test
void getLabelsByTypeId_shouldReturnOk() {
CourseLabel label = new CourseLabel();
label.setId(1L);
label.setLabelName("适合新手");
when(courseLabelService.findByTypeId(1L)).thenReturn(Flux.just(label));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("typeId", "1")
.build();
Mono<ServerResponse> result = handler.getLabelsByTypeId(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(courseLabelService).findByTypeId(1L);
}
@Test
void getLabelsByTypeId_shouldReturnOkWhenEmpty() {
when(courseLabelService.findByTypeId(1L)).thenReturn(Flux.empty());
MockServerRequest request = MockServerRequest.builder()
.pathVariable("typeId", "1")
.build();
Mono<ServerResponse> result = handler.getLabelsByTypeId(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
}
// ==================== addLabelsToType ====================
@Test
void addLabelsToType_shouldReturnOk() {
when(courseLabelService.addLabelsToType(1L, List.of(1L, 2L))).thenReturn(Mono.empty());
MockServerRequest request = MockServerRequest.builder()
.pathVariable("typeId", "1")
.body(Mono.just(Map.of("labelIds", List.of(1L, 2L))));
Mono<ServerResponse> result = handler.addLabelsToType(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(courseLabelService).addLabelsToType(1L, List.of(1L, 2L));
}
@Test
void addLabelsToType_shouldReturn400WhenLabelIdsEmpty() {
MockServerRequest request = MockServerRequest.builder()
.pathVariable("typeId", "1")
.body(Mono.just(Map.of("labelIds", List.of())));
Mono<ServerResponse> result = handler.addLabelsToType(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
verify(courseLabelService, never()).addLabelsToType(anyLong(), anyList());
}
@Test
void addLabelsToType_shouldReturn400WhenLabelIdsNull() {
MockServerRequest request = MockServerRequest.builder()
.pathVariable("typeId", "1")
.body(Mono.just(Map.of()));
Mono<ServerResponse> result = handler.addLabelsToType(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
verify(courseLabelService, never()).addLabelsToType(anyLong(), anyList());
}
@Test
void addLabelsToType_shouldReturn400WhenServiceError() {
when(courseLabelService.addLabelsToType(1L, List.of(1L))).thenReturn(Mono.error(new RuntimeException("添加失败")));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("typeId", "1")
.body(Mono.just(Map.of("labelIds", List.of(1L))));
Mono<ServerResponse> result = handler.addLabelsToType(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
// ==================== removeLabelFromType ====================
@Test
void removeLabelFromType_shouldReturnOk() {
when(courseLabelService.removeLabelFromType(1L, 1L)).thenReturn(Mono.empty());
MockServerRequest request = MockServerRequest.builder()
.pathVariable("typeId", "1")
.pathVariable("labelId", "1")
.build();
Mono<ServerResponse> result = handler.removeLabelFromType(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(courseLabelService).removeLabelFromType(1L, 1L);
}
@Test
void removeLabelFromType_shouldReturn400WhenServiceError() {
when(courseLabelService.removeLabelFromType(1L, 1L)).thenReturn(Mono.error(new RuntimeException("移除失败")));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("typeId", "1")
.pathVariable("labelId", "1")
.build();
Mono<ServerResponse> result = handler.removeLabelFromType(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
// ==================== clearLabelsFromType ====================
@Test
void clearLabelsFromType_shouldReturnOk() {
when(courseLabelService.clearLabelsFromType(1L)).thenReturn(Mono.empty());
MockServerRequest request = MockServerRequest.builder()
.pathVariable("typeId", "1")
.build();
Mono<ServerResponse> result = handler.clearLabelsFromType(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(courseLabelService).clearLabelsFromType(1L);
}
@Test
void clearLabelsFromType_shouldReturn400WhenServiceError() {
when(courseLabelService.clearLabelsFromType(1L)).thenReturn(Mono.error(new RuntimeException("清空失败")));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("typeId", "1")
.build();
Mono<ServerResponse> result = handler.clearLabelsFromType(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
}
@@ -0,0 +1,214 @@
package cn.novalon.gym.manage.groupcourse.handler;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseBooking;
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseBookingService;
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.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class GroupCourseBookingHandlerTest {
@Mock
private IGroupCourseBookingService bookingService;
private GroupCourseBookingHandler handler;
@BeforeEach
void setUp() {
handler = new GroupCourseBookingHandler(bookingService);
}
// ==================== bookCourse ====================
@Test
void bookCourse_shouldReturnOk() {
GroupCourseBooking booking = new GroupCourseBooking();
booking.setId(1L);
booking.setCourseId(10L);
booking.setMemberId(100L);
when(bookingService.bookCourse(10L, 100L)).thenReturn(Mono.just(booking));
MockServerRequest request = MockServerRequest.builder()
.body(Mono.just(Map.of("courseId", 10L, "memberId", 100L)));
Mono<ServerResponse> result = handler.bookCourse(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(bookingService).bookCourse(10L, 100L);
}
@Test
void bookCourse_shouldReturn400WhenCourseIdMissing() {
MockServerRequest request = MockServerRequest.builder()
.body(Mono.just(Map.of("memberId", 100L)));
Mono<ServerResponse> result = handler.bookCourse(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
verify(bookingService, never()).bookCourse(anyLong(), anyLong());
}
@Test
void bookCourse_shouldReturn400WhenMemberIdMissing() {
MockServerRequest request = MockServerRequest.builder()
.body(Mono.just(Map.of("courseId", 10L)));
Mono<ServerResponse> result = handler.bookCourse(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
verify(bookingService, never()).bookCourse(anyLong(), anyLong());
}
@Test
void bookCourse_shouldReturn400WhenServiceError() {
when(bookingService.bookCourse(10L, 100L)).thenReturn(Mono.error(new RuntimeException("预约失败,课程已满")));
MockServerRequest request = MockServerRequest.builder()
.body(Mono.just(Map.of("courseId", 10L, "memberId", 100L)));
Mono<ServerResponse> result = handler.bookCourse(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
// ==================== cancelBooking ====================
@Test
void cancelBooking_shouldReturnOk() {
GroupCourseBooking booking = new GroupCourseBooking();
booking.setId(1L);
booking.setStatus("1");
when(bookingService.cancelBooking(1L, 100L)).thenReturn(Mono.just(booking));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("bookingId", "1")
.body(Mono.just(Map.of("memberId", 100L)));
Mono<ServerResponse> result = handler.cancelBooking(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(bookingService).cancelBooking(1L, 100L);
}
@Test
void cancelBooking_shouldReturn400WhenServiceError() {
when(bookingService.cancelBooking(1L, 100L)).thenReturn(Mono.error(new RuntimeException("取消失败")));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("bookingId", "1")
.body(Mono.just(Map.of("memberId", 100L)));
Mono<ServerResponse> result = handler.cancelBooking(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
// ==================== getBookingsByMemberId ====================
@Test
void getBookingsByMemberId_shouldReturnOk() {
GroupCourseBooking b1 = new GroupCourseBooking();
b1.setId(1L);
GroupCourseBooking b2 = new GroupCourseBooking();
b2.setId(2L);
when(bookingService.getBookingsByMemberId(100L)).thenReturn(Flux.just(b1, b2));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("memberId", "100")
.build();
Mono<ServerResponse> result = handler.getBookingsByMemberId(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(bookingService).getBookingsByMemberId(100L);
}
@Test
void getBookingsByMemberId_shouldReturnOkWhenEmpty() {
when(bookingService.getBookingsByMemberId(999L)).thenReturn(Flux.empty());
MockServerRequest request = MockServerRequest.builder()
.pathVariable("memberId", "999")
.build();
Mono<ServerResponse> result = handler.getBookingsByMemberId(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
}
// ==================== getBookingById ====================
@Test
void getBookingById_shouldReturnOkWhenFound() {
GroupCourseBooking booking = new GroupCourseBooking();
booking.setId(1L);
when(bookingService.getBookingById(1L)).thenReturn(Mono.just(booking));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("bookingId", "1")
.build();
Mono<ServerResponse> result = handler.getBookingById(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(bookingService).getBookingById(1L);
}
@Test
void getBookingById_shouldReturnNotFound() {
when(bookingService.getBookingById(999L)).thenReturn(Mono.empty());
MockServerRequest request = MockServerRequest.builder()
.pathVariable("bookingId", "999")
.build();
Mono<ServerResponse> result = handler.getBookingById(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
// ==================== getBookingsByCourseId ====================
@Test
void getBookingsByCourseId_shouldReturnOk() {
GroupCourseBooking b1 = new GroupCourseBooking();
b1.setId(1L);
when(bookingService.getBookingsByCourseId(10L)).thenReturn(Flux.just(b1));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("courseId", "10")
.build();
Mono<ServerResponse> result = handler.getBookingsByCourseId(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(bookingService).getBookingsByCourseId(10L);
}
@Test
void getBookingsByCourseId_shouldReturnOkWhenEmpty() {
when(bookingService.getBookingsByCourseId(999L)).thenReturn(Flux.empty());
MockServerRequest request = MockServerRequest.builder()
.pathVariable("courseId", "999")
.build();
Mono<ServerResponse> result = handler.getBookingsByCourseId(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
}
}
@@ -195,10 +195,10 @@ class GroupCourseHandlerTest {
@Test
void signIn_shouldReturnOk() {
GroupCourse course = createTestCourse(1L, "瑜伽课");
when(validator.validate(any())).thenReturn(java.util.Collections.emptySet());
when(groupCourseService.signIn(eq(1L), eq(10001L))).thenReturn(Mono.just(course));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("memberId", "10001")
.pathVariable("courseId", "1")
.body(Mono.just(java.util.Map.of("memberId", 10001L, "courseId", 1L)));
@@ -0,0 +1,357 @@
package cn.novalon.gym.manage.groupcourse.handler;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseRecommend;
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseRecommendService;
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.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class GroupCourseRecommendHandlerTest {
@Mock
private IGroupCourseRecommendService recommendService;
private GroupCourseRecommendHandler handler;
@BeforeEach
void setUp() {
handler = new GroupCourseRecommendHandler(recommendService);
}
// ==================== getAllRecommendations ====================
@Test
void getAllRecommendations_shouldReturnOk() {
GroupCourseRecommend r1 = new GroupCourseRecommend();
r1.setId(1L);
r1.setRecommendTitle("本周热门");
GroupCourseRecommend r2 = new GroupCourseRecommend();
r2.setId(2L);
r2.setRecommendTitle("新课程推荐");
when(recommendService.findAll("priority", "desc")).thenReturn(Flux.just(r1, r2));
MockServerRequest request = MockServerRequest.builder().build();
Mono<ServerResponse> result = handler.getAllRecommendations(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(recommendService).findAll("priority", "desc");
}
@Test
void getAllRecommendations_shouldRespectSortParams() {
GroupCourseRecommend r = new GroupCourseRecommend();
r.setId(1L);
when(recommendService.findAll("id", "asc")).thenReturn(Flux.just(r));
MockServerRequest request = MockServerRequest.builder()
.queryParam("sortBy", "id")
.queryParam("sortOrder", "asc")
.build();
Mono<ServerResponse> result = handler.getAllRecommendations(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(recommendService).findAll("id", "asc");
}
@Test
void getAllRecommendations_shouldReturnOkWhenEmpty() {
when(recommendService.findAll("priority", "desc")).thenReturn(Flux.empty());
MockServerRequest request = MockServerRequest.builder().build();
Mono<ServerResponse> result = handler.getAllRecommendations(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
}
// ==================== getAllActiveRecommendations ====================
@Test
void getAllActiveRecommendations_shouldReturnOk() {
GroupCourseRecommend r = new GroupCourseRecommend();
r.setId(1L);
r.setIsActive(true);
when(recommendService.findAllActive()).thenReturn(Flux.just(r));
MockServerRequest request = MockServerRequest.builder().build();
Mono<ServerResponse> result = handler.getAllActiveRecommendations(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(recommendService).findAllActive();
}
@Test
void getAllActiveRecommendations_shouldReturnOkWhenEmpty() {
when(recommendService.findAllActive()).thenReturn(Flux.empty());
MockServerRequest request = MockServerRequest.builder().build();
Mono<ServerResponse> result = handler.getAllActiveRecommendations(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
}
// ==================== getRecommendationById ====================
@Test
void getRecommendationById_shouldReturnOkWhenFound() {
GroupCourseRecommend r = new GroupCourseRecommend();
r.setId(1L);
r.setRecommendTitle("本周热门");
when(recommendService.findById(1L)).thenReturn(Mono.just(r));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("id", "1")
.build();
Mono<ServerResponse> result = handler.getRecommendationById(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(recommendService).findById(1L);
}
@Test
void getRecommendationById_shouldReturnNotFound() {
when(recommendService.findById(999L)).thenReturn(Mono.empty());
MockServerRequest request = MockServerRequest.builder()
.pathVariable("id", "999")
.build();
Mono<ServerResponse> result = handler.getRecommendationById(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
// ==================== getRecommendationsByCourseId ====================
@Test
void getRecommendationsByCourseId_shouldReturnOk() {
GroupCourseRecommend r = new GroupCourseRecommend();
r.setId(1L);
r.setCourseId(10L);
when(recommendService.findByCourseId(10L)).thenReturn(Flux.just(r));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("courseId", "10")
.build();
Mono<ServerResponse> result = handler.getRecommendationsByCourseId(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(recommendService).findByCourseId(10L);
}
@Test
void getRecommendationsByCourseId_shouldReturnOkWhenEmpty() {
when(recommendService.findByCourseId(999L)).thenReturn(Flux.empty());
MockServerRequest request = MockServerRequest.builder()
.pathVariable("courseId", "999")
.build();
Mono<ServerResponse> result = handler.getRecommendationsByCourseId(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
}
// ==================== createRecommendation ====================
@Test
void createRecommendation_shouldReturnOk() {
GroupCourseRecommend recommend = new GroupCourseRecommend();
recommend.setCourseId(10L);
recommend.setRecommendTitle("本周热门");
recommend.setPriority(10);
GroupCourseRecommend created = new GroupCourseRecommend();
created.setId(1L);
created.setCourseId(10L);
created.setRecommendTitle("本周热门");
created.setPriority(10);
when(recommendService.create(any(GroupCourseRecommend.class))).thenReturn(Mono.just(created));
MockServerRequest request = MockServerRequest.builder()
.body(Mono.just(recommend));
Mono<ServerResponse> result = handler.createRecommendation(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(recommendService).create(any(GroupCourseRecommend.class));
}
@Test
void createRecommendation_shouldReturn400WhenCourseIdNull() {
GroupCourseRecommend recommend = new GroupCourseRecommend();
recommend.setRecommendTitle("本周热门");
MockServerRequest request = MockServerRequest.builder()
.body(Mono.just(recommend));
Mono<ServerResponse> result = handler.createRecommendation(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
verify(recommendService, never()).create(any());
}
@Test
void createRecommendation_shouldReturn400WhenServiceError() {
GroupCourseRecommend recommend = new GroupCourseRecommend();
recommend.setCourseId(10L);
when(recommendService.create(any(GroupCourseRecommend.class)))
.thenReturn(Mono.error(new RuntimeException("推荐已存在")));
MockServerRequest request = MockServerRequest.builder()
.body(Mono.just(recommend));
Mono<ServerResponse> result = handler.createRecommendation(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
// ==================== updateRecommendation ====================
@Test
void updateRecommendation_shouldReturnOk() {
GroupCourseRecommend recommend = new GroupCourseRecommend();
recommend.setRecommendTitle("更新推荐");
GroupCourseRecommend updated = new GroupCourseRecommend();
updated.setId(1L);
updated.setRecommendTitle("更新推荐");
when(recommendService.update(eq(1L), any(GroupCourseRecommend.class))).thenReturn(Mono.just(updated));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("id", "1")
.body(Mono.just(recommend));
Mono<ServerResponse> result = handler.updateRecommendation(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(recommendService).update(eq(1L), any(GroupCourseRecommend.class));
}
@Test
void updateRecommendation_shouldReturn400WhenServiceError() {
GroupCourseRecommend recommend = new GroupCourseRecommend();
recommend.setRecommendTitle("更新推荐");
when(recommendService.update(eq(1L), any(GroupCourseRecommend.class)))
.thenReturn(Mono.error(new RuntimeException("推荐不存在")));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("id", "1")
.body(Mono.just(recommend));
Mono<ServerResponse> result = handler.updateRecommendation(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
// ==================== deleteRecommendation ====================
@Test
void deleteRecommendation_shouldReturnOk() {
when(recommendService.delete(1L)).thenReturn(Mono.empty());
MockServerRequest request = MockServerRequest.builder()
.pathVariable("id", "1")
.build();
Mono<ServerResponse> result = handler.deleteRecommendation(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(recommendService).delete(1L);
}
@Test
void deleteRecommendation_shouldReturn400WhenServiceError() {
when(recommendService.delete(1L)).thenReturn(Mono.error(new RuntimeException("删除失败")));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("id", "1")
.build();
Mono<ServerResponse> result = handler.deleteRecommendation(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
// ==================== enableRecommendation ====================
@Test
void enableRecommendation_shouldReturnOk() {
GroupCourseRecommend r = new GroupCourseRecommend();
r.setId(1L);
r.setIsActive(true);
when(recommendService.enable(1L)).thenReturn(Mono.just(r));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("id", "1")
.build();
Mono<ServerResponse> result = handler.enableRecommendation(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(recommendService).enable(1L);
}
@Test
void enableRecommendation_shouldReturn400WhenServiceError() {
when(recommendService.enable(1L)).thenReturn(Mono.error(new RuntimeException("启用失败")));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("id", "1")
.build();
Mono<ServerResponse> result = handler.enableRecommendation(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
// ==================== disableRecommendation ====================
@Test
void disableRecommendation_shouldReturnOk() {
GroupCourseRecommend r = new GroupCourseRecommend();
r.setId(1L);
r.setIsActive(false);
when(recommendService.disable(1L)).thenReturn(Mono.just(r));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("id", "1")
.build();
Mono<ServerResponse> result = handler.disableRecommendation(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(recommendService).disable(1L);
}
@Test
void disableRecommendation_shouldReturn400WhenServiceError() {
when(recommendService.disable(1L)).thenReturn(Mono.error(new RuntimeException("禁用失败")));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("id", "1")
.build();
Mono<ServerResponse> result = handler.disableRecommendation(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
}
@@ -0,0 +1,375 @@
package cn.novalon.gym.manage.groupcourse.handler;
import cn.novalon.gym.manage.common.dto.PageRequest;
import cn.novalon.gym.manage.common.dto.PageResponse;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseType;
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseTypeService;
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.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class GroupCourseTypeHandlerTest {
@Mock
private IGroupCourseTypeService groupCourseTypeService;
private GroupCourseTypeHandler handler;
@BeforeEach
void setUp() {
handler = new GroupCourseTypeHandler(groupCourseTypeService);
}
// ==================== getAllGroupCourseTypes ====================
@Test
void getAllGroupCourseTypes_shouldReturnOk() {
GroupCourseType type1 = new GroupCourseType();
type1.setId(1L);
type1.setTypeName("瑜伽入门");
GroupCourseType type2 = new GroupCourseType();
type2.setId(2L);
type2.setTypeName("力量训练");
when(groupCourseTypeService.findAll(false)).thenReturn(Flux.just(type1, type2));
MockServerRequest request = MockServerRequest.builder().build();
Mono<ServerResponse> result = handler.getAllGroupCourseTypes(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(groupCourseTypeService).findAll(false);
}
@Test
void getAllGroupCourseTypes_shouldIncludeDeletedWhenParamSet() {
GroupCourseType type = new GroupCourseType();
type.setId(1L);
type.setTypeName("测试");
when(groupCourseTypeService.findAll(true)).thenReturn(Flux.just(type));
MockServerRequest request = MockServerRequest.builder()
.queryParam("includeDeleted", "true")
.build();
Mono<ServerResponse> result = handler.getAllGroupCourseTypes(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(groupCourseTypeService).findAll(true);
}
@Test
void getAllGroupCourseTypes_shouldReturnOkWhenEmpty() {
when(groupCourseTypeService.findAll(false)).thenReturn(Flux.empty());
MockServerRequest request = MockServerRequest.builder().build();
Mono<ServerResponse> result = handler.getAllGroupCourseTypes(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
}
// ==================== getGroupCourseTypeById ====================
@Test
void getGroupCourseTypeById_shouldReturnOkWhenFound() {
GroupCourseType type = new GroupCourseType();
type.setId(1L);
type.setTypeName("瑜伽入门");
when(groupCourseTypeService.findById(1L)).thenReturn(Mono.just(type));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("id", "1")
.build();
Mono<ServerResponse> result = handler.getGroupCourseTypeById(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(groupCourseTypeService).findById(1L);
}
@Test
void getGroupCourseTypeById_shouldReturnNotFound() {
when(groupCourseTypeService.findById(999L)).thenReturn(Mono.empty());
MockServerRequest request = MockServerRequest.builder()
.pathVariable("id", "999")
.build();
Mono<ServerResponse> result = handler.getGroupCourseTypeById(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
// ==================== searchGroupCourseTypes ====================
@Test
void searchGroupCourseTypes_shouldReturnOk() {
GroupCourseType type = new GroupCourseType();
type.setId(1L);
type.setTypeName("瑜伽");
when(groupCourseTypeService.findByKeyword("瑜伽")).thenReturn(Flux.just(type));
MockServerRequest request = MockServerRequest.builder()
.queryParam("keyword", "瑜伽")
.build();
Mono<ServerResponse> result = handler.searchGroupCourseTypes(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(groupCourseTypeService).findByKeyword("瑜伽");
}
@Test
void searchGroupCourseTypes_shouldReturnOkWhenEmpty() {
when(groupCourseTypeService.findByKeyword("")).thenReturn(Flux.empty());
MockServerRequest request = MockServerRequest.builder().build();
Mono<ServerResponse> result = handler.searchGroupCourseTypes(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
}
// ==================== getGroupCourseTypesByCategory ====================
@Test
void getGroupCourseTypesByCategory_shouldReturnOk() {
GroupCourseType type = new GroupCourseType();
type.setId(1L);
type.setTypeName("瑜伽入门");
when(groupCourseTypeService.findByCategoryAndKeyword("柔韧与平衡类", "")).thenReturn(Flux.just(type));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("category", "柔韧与平衡类")
.build();
Mono<ServerResponse> result = handler.getGroupCourseTypesByCategory(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(groupCourseTypeService).findByCategoryAndKeyword("柔韧与平衡类", "");
}
@Test
void getGroupCourseTypesByCategory_shouldReturnOkWithKeyword() {
GroupCourseType type = new GroupCourseType();
type.setId(1L);
type.setTypeName("瑜伽入门");
when(groupCourseTypeService.findByCategoryAndKeyword("柔韧与平衡类", "瑜伽")).thenReturn(Flux.just(type));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("category", "柔韧与平衡类")
.queryParam("keyword", "瑜伽")
.build();
Mono<ServerResponse> result = handler.getGroupCourseTypesByCategory(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(groupCourseTypeService).findByCategoryAndKeyword("柔韧与平衡类", "瑜伽");
}
// ==================== getCategories ====================
@Test
void getCategories_shouldReturnOk() {
when(groupCourseTypeService.findCategories()).thenReturn(Flux.just("有氧类", "力量类", "柔韧与平衡类"));
MockServerRequest request = MockServerRequest.builder().build();
Mono<ServerResponse> result = handler.getCategories(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(groupCourseTypeService).findCategories();
}
@Test
void getCategories_shouldReturnOkWhenEmpty() {
when(groupCourseTypeService.findCategories()).thenReturn(Flux.empty());
MockServerRequest request = MockServerRequest.builder().build();
Mono<ServerResponse> result = handler.getCategories(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
}
// ==================== createGroupCourseType ====================
@Test
void createGroupCourseType_shouldReturnOk() {
GroupCourseType type = new GroupCourseType();
type.setTypeName("瑜伽入门");
type.setBaseDifficulty(2);
GroupCourseType created = new GroupCourseType();
created.setId(1L);
created.setTypeName("瑜伽入门");
created.setBaseDifficulty(2);
when(groupCourseTypeService.create(any(GroupCourseType.class))).thenReturn(Mono.just(created));
MockServerRequest request = MockServerRequest.builder()
.body(Mono.just(type));
Mono<ServerResponse> result = handler.createGroupCourseType(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(groupCourseTypeService).create(any(GroupCourseType.class));
}
@Test
void createGroupCourseType_shouldReturn400WhenNameEmpty() {
GroupCourseType type = new GroupCourseType();
type.setTypeName("");
MockServerRequest request = MockServerRequest.builder()
.body(Mono.just(type));
Mono<ServerResponse> result = handler.createGroupCourseType(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
verify(groupCourseTypeService, never()).create(any());
}
@Test
void createGroupCourseType_shouldReturn400WhenNameNull() {
GroupCourseType type = new GroupCourseType();
MockServerRequest request = MockServerRequest.builder()
.body(Mono.just(type));
Mono<ServerResponse> result = handler.createGroupCourseType(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
verify(groupCourseTypeService, never()).create(any());
}
@Test
void createGroupCourseType_shouldSetDefaultDifficulty() {
GroupCourseType type = new GroupCourseType();
type.setTypeName("瑜伽入门");
GroupCourseType created = new GroupCourseType();
created.setId(1L);
created.setTypeName("瑜伽入门");
created.setBaseDifficulty(1);
when(groupCourseTypeService.create(any(GroupCourseType.class))).thenReturn(Mono.just(created));
MockServerRequest request = MockServerRequest.builder()
.body(Mono.just(type));
Mono<ServerResponse> result = handler.createGroupCourseType(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
}
@Test
void createGroupCourseType_shouldReturn400WhenServiceError() {
GroupCourseType type = new GroupCourseType();
type.setTypeName("瑜伽入门");
when(groupCourseTypeService.create(any(GroupCourseType.class)))
.thenReturn(Mono.error(new RuntimeException("类型已存在")));
MockServerRequest request = MockServerRequest.builder()
.body(Mono.just(type));
Mono<ServerResponse> result = handler.createGroupCourseType(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
// ==================== updateGroupCourseType ====================
@Test
void updateGroupCourseType_shouldReturnOk() {
GroupCourseType type = new GroupCourseType();
type.setTypeName("瑜伽进阶");
type.setBaseDifficulty(4);
GroupCourseType updated = new GroupCourseType();
updated.setId(1L);
updated.setTypeName("瑜伽进阶");
updated.setBaseDifficulty(4);
when(groupCourseTypeService.update(eq(1L), any(GroupCourseType.class))).thenReturn(Mono.just(updated));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("id", "1")
.body(Mono.just(type));
Mono<ServerResponse> result = handler.updateGroupCourseType(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(groupCourseTypeService).update(eq(1L), any(GroupCourseType.class));
}
@Test
void updateGroupCourseType_shouldReturn400WhenServiceError() {
GroupCourseType type = new GroupCourseType();
type.setTypeName("瑜伽进阶");
when(groupCourseTypeService.update(eq(1L), any(GroupCourseType.class)))
.thenReturn(Mono.error(new RuntimeException("类型不存在")));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("id", "1")
.body(Mono.just(type));
Mono<ServerResponse> result = handler.updateGroupCourseType(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
// ==================== deleteGroupCourseType ====================
@Test
void deleteGroupCourseType_shouldReturnOk() {
when(groupCourseTypeService.delete(1L)).thenReturn(Mono.empty());
MockServerRequest request = MockServerRequest.builder()
.pathVariable("id", "1")
.build();
Mono<ServerResponse> result = handler.deleteGroupCourseType(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(groupCourseTypeService).delete(1L);
}
@Test
void deleteGroupCourseType_shouldReturn400WhenServiceError() {
when(groupCourseTypeService.delete(1L)).thenReturn(Mono.error(new RuntimeException("删除失败")));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("id", "1")
.build();
Mono<ServerResponse> result = handler.deleteGroupCourseType(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
// ==================== getGroupCourseTypesByPage ====================
@Test
void getGroupCourseTypesByPage_shouldReturnOk() {
PageRequest pageRequest = new PageRequest();
PageResponse<GroupCourseType> pageResponse = new PageResponse<>();
when(groupCourseTypeService.findByPage(any(PageRequest.class))).thenReturn(Mono.just(pageResponse));
MockServerRequest request = MockServerRequest.builder()
.body(Mono.just(pageRequest));
Mono<ServerResponse> result = handler.getGroupCourseTypesByPage(request);
ServerResponse response = result.block();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
verify(groupCourseTypeService).findByPage(any(PageRequest.class));
}
}
@@ -0,0 +1,434 @@
package cn.novalon.gym.manage.groupcourse.service.impl;
import cn.novalon.gym.manage.common.dto.PageRequest;
import cn.novalon.gym.manage.common.dto.PageResponse;
import cn.novalon.gym.manage.common.util.RedisUtil;
import cn.novalon.gym.manage.groupcourse.domain.CourseLabel;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import cn.novalon.gym.manage.groupcourse.repository.ICourseLabelRepository;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRepository;
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 reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CourseLabelServiceTest {
@Mock
private ICourseLabelRepository courseLabelRepository;
@Mock
private IGroupCourseRepository groupCourseRepository;
@Mock
private RedisUtil redisUtil;
private CourseLabelService courseLabelService;
@BeforeEach
void setUp() {
courseLabelService = new CourseLabelService(courseLabelRepository, groupCourseRepository, redisUtil);
}
// ==================== findById ====================
@Test
void findById_shouldReturnLabel() {
CourseLabel label = new CourseLabel();
label.setId(1L);
label.setLabelName("适合新手");
when(courseLabelRepository.findById(1L)).thenReturn(Mono.just(label));
StepVerifier.create(courseLabelService.findById(1L))
.assertNext(result -> {
assertThat(result.getId()).isEqualTo(1L);
assertThat(result.getLabelName()).isEqualTo("适合新手");
})
.verifyComplete();
verify(courseLabelRepository).findById(1L);
}
@Test
void findById_shouldReturnEmptyWhenNotFound() {
when(courseLabelRepository.findById(999L)).thenReturn(Mono.empty());
StepVerifier.create(courseLabelService.findById(999L))
.verifyComplete();
verify(courseLabelRepository).findById(999L);
}
// ==================== findAll ====================
@Test
void findAll_shouldReturnAllLabels() {
CourseLabel label1 = new CourseLabel();
label1.setId(1L);
label1.setLabelName("适合新手");
CourseLabel label2 = new CourseLabel();
label2.setId(2L);
label2.setLabelName("高强度");
when(courseLabelRepository.findAll()).thenReturn(Flux.just(label1, label2));
StepVerifier.create(courseLabelService.findAll())
.assertNext(result -> assertThat(result.getLabelName()).isEqualTo("适合新手"))
.assertNext(result -> assertThat(result.getLabelName()).isEqualTo("高强度"))
.verifyComplete();
verify(courseLabelRepository).findAll();
}
@Test
void findAll_shouldReturnEmptyWhenNoLabels() {
when(courseLabelRepository.findAll()).thenReturn(Flux.empty());
StepVerifier.create(courseLabelService.findAll())
.verifyComplete();
verify(courseLabelRepository).findAll();
}
// ==================== findByKeyword ====================
@Test
void findByKeyword_shouldReturnMatchingLabels() {
CourseLabel label = new CourseLabel();
label.setId(1L);
label.setLabelName("适合新手");
when(courseLabelRepository.findByKeyword("新手")).thenReturn(Flux.just(label));
StepVerifier.create(courseLabelService.findByKeyword("新手"))
.assertNext(result -> {
assertThat(result.getId()).isEqualTo(1L);
assertThat(result.getLabelName()).isEqualTo("适合新手");
})
.verifyComplete();
verify(courseLabelRepository).findByKeyword("新手");
}
@Test
void findByKeyword_shouldReturnEmptyWhenNoMatch() {
when(courseLabelRepository.findByKeyword("不存在")).thenReturn(Flux.empty());
StepVerifier.create(courseLabelService.findByKeyword("不存在"))
.verifyComplete();
verify(courseLabelRepository).findByKeyword("不存在");
}
// ==================== create ====================
@Test
void create_shouldSaveWhenNameNotExists() {
CourseLabel input = new CourseLabel();
input.setLabelName("适合新手");
input.setColor("#52c41a");
CourseLabel saved = new CourseLabel();
saved.setId(1L);
saved.setLabelName("适合新手");
saved.setColor("#52c41a");
when(courseLabelRepository.findByLabelName("适合新手")).thenReturn(Mono.empty());
when(courseLabelRepository.save(any(CourseLabel.class))).thenReturn(Mono.just(saved));
StepVerifier.create(courseLabelService.create(input))
.assertNext(result -> {
assertThat(result.getId()).isEqualTo(1L);
assertThat(result.getLabelName()).isEqualTo("适合新手");
})
.verifyComplete();
verify(courseLabelRepository).findByLabelName("适合新手");
verify(courseLabelRepository).save(any(CourseLabel.class));
}
@Test
void create_shouldErrorWhenNameAlreadyExists() {
CourseLabel existing = new CourseLabel();
existing.setId(1L);
existing.setLabelName("适合新手");
CourseLabel input = new CourseLabel();
input.setLabelName("适合新手");
when(courseLabelRepository.findByLabelName("适合新手")).thenReturn(Mono.just(existing));
// switchIfEmpty 是急切求值的,需要为 save 提供 stub 避免 NPE
lenient().when(courseLabelRepository.save(any(CourseLabel.class))).thenReturn(Mono.just(new CourseLabel()));
StepVerifier.create(courseLabelService.create(input))
.expectErrorMatches(throwable ->
throwable instanceof RuntimeException &&
"标签名称已存在".equals(throwable.getMessage()))
.verify();
verify(courseLabelRepository).findByLabelName("适合新手");
// switchIfEmpty 是急切求值的,save 会被调用但不会影响结果
}
// ==================== update ====================
@Test
void update_shouldUpdateLabel() {
CourseLabel input = new CourseLabel();
input.setLabelName("适合进阶");
CourseLabel updated = new CourseLabel();
updated.setId(1L);
updated.setLabelName("适合进阶");
when(courseLabelRepository.update(any(CourseLabel.class))).thenReturn(Mono.just(updated));
StepVerifier.create(courseLabelService.update(1L, input))
.assertNext(result -> {
assertThat(result.getId()).isEqualTo(1L);
assertThat(result.getLabelName()).isEqualTo("适合进阶");
})
.verifyComplete();
verify(courseLabelRepository).update(any(CourseLabel.class));
}
@Test
void update_shouldSetIdOnInput() {
CourseLabel input = new CourseLabel();
input.setLabelName("适合进阶");
when(courseLabelRepository.update(any(CourseLabel.class))).thenAnswer(invocation -> {
CourseLabel arg = invocation.getArgument(0);
return Mono.just(arg);
});
StepVerifier.create(courseLabelService.update(1L, input))
.assertNext(result -> assertThat(result.getId()).isEqualTo(1L))
.verifyComplete();
}
// ==================== delete ====================
@Test
void delete_shouldDeleteLabel() {
when(courseLabelRepository.deleteById(1L)).thenReturn(Mono.empty());
StepVerifier.create(courseLabelService.delete(1L))
.verifyComplete();
verify(courseLabelRepository).deleteById(1L);
}
@Test
void delete_shouldCompleteWhenNotFound() {
when(courseLabelRepository.deleteById(999L)).thenReturn(Mono.empty());
StepVerifier.create(courseLabelService.delete(999L))
.verifyComplete();
verify(courseLabelRepository).deleteById(999L);
}
// ==================== findByTypeId ====================
@Test
void findByTypeId_shouldReturnLabelsForType() {
CourseLabel label1 = new CourseLabel();
label1.setId(1L);
label1.setLabelName("适合新手");
CourseLabel label2 = new CourseLabel();
label2.setId(2L);
label2.setLabelName("减脂塑形");
when(courseLabelRepository.findByTypeId(1L)).thenReturn(Flux.just(label1, label2));
StepVerifier.create(courseLabelService.findByTypeId(1L))
.assertNext(result -> assertThat(result.getLabelName()).isEqualTo("适合新手"))
.assertNext(result -> assertThat(result.getLabelName()).isEqualTo("减脂塑形"))
.verifyComplete();
verify(courseLabelRepository).findByTypeId(1L);
}
@Test
void findByTypeId_shouldReturnEmptyWhenNoLabels() {
when(courseLabelRepository.findByTypeId(1L)).thenReturn(Flux.empty());
StepVerifier.create(courseLabelService.findByTypeId(1L))
.verifyComplete();
verify(courseLabelRepository).findByTypeId(1L);
}
// ==================== addLabelsToType ====================
@Test
void addLabelsToType_shouldAddLabelsAndInvalidateCache() {
when(courseLabelRepository.addLabelsToType(1L, List.of(1L, 2L))).thenReturn(Mono.empty());
GroupCourse course1 = new GroupCourse();
course1.setId(101L);
GroupCourse course2 = new GroupCourse();
course2.setId(102L);
when(groupCourseRepository.findByCourseType(1L)).thenReturn(Flux.just(course1, course2));
when(redisUtil.delete("group_course:detail:101")).thenReturn(Mono.just(1L));
when(redisUtil.delete("group_course:detail:102")).thenReturn(Mono.just(1L));
StepVerifier.create(courseLabelService.addLabelsToType(1L, List.of(1L, 2L)))
.verifyComplete();
verify(courseLabelRepository).addLabelsToType(1L, List.of(1L, 2L));
verify(groupCourseRepository).findByCourseType(1L);
verify(redisUtil).delete("group_course:detail:101");
verify(redisUtil).delete("group_course:detail:102");
}
@Test
void addLabelsToType_shouldNotInvalidateCacheWhenNoCourses() {
when(courseLabelRepository.addLabelsToType(1L, List.of(1L))).thenReturn(Mono.empty());
when(groupCourseRepository.findByCourseType(1L)).thenReturn(Flux.empty());
StepVerifier.create(courseLabelService.addLabelsToType(1L, List.of(1L)))
.verifyComplete();
verify(courseLabelRepository).addLabelsToType(1L, List.of(1L));
verify(groupCourseRepository).findByCourseType(1L);
verify(redisUtil, never()).delete(anyString());
}
// ==================== removeLabelFromType ====================
@Test
void removeLabelFromType_shouldRemoveLabelAndInvalidateCache() {
when(courseLabelRepository.removeLabelFromType(1L, 1L)).thenReturn(Mono.empty());
GroupCourse course = new GroupCourse();
course.setId(101L);
when(groupCourseRepository.findByCourseType(1L)).thenReturn(Flux.just(course));
when(redisUtil.delete("group_course:detail:101")).thenReturn(Mono.just(1L));
StepVerifier.create(courseLabelService.removeLabelFromType(1L, 1L))
.verifyComplete();
verify(courseLabelRepository).removeLabelFromType(1L, 1L);
verify(groupCourseRepository).findByCourseType(1L);
verify(redisUtil).delete("group_course:detail:101");
}
@Test
void removeLabelFromType_shouldNotInvalidateCacheWhenNoCourses() {
when(courseLabelRepository.removeLabelFromType(1L, 1L)).thenReturn(Mono.empty());
when(groupCourseRepository.findByCourseType(1L)).thenReturn(Flux.empty());
StepVerifier.create(courseLabelService.removeLabelFromType(1L, 1L))
.verifyComplete();
verify(courseLabelRepository).removeLabelFromType(1L, 1L);
verify(redisUtil, never()).delete(anyString());
}
// ==================== clearLabelsFromType ====================
@Test
void clearLabelsFromType_shouldClearLabelsAndInvalidateCache() {
when(courseLabelRepository.clearLabelsFromType(1L)).thenReturn(Mono.empty());
GroupCourse course1 = new GroupCourse();
course1.setId(101L);
GroupCourse course2 = new GroupCourse();
course2.setId(102L);
when(groupCourseRepository.findByCourseType(1L)).thenReturn(Flux.just(course1, course2));
when(redisUtil.delete("group_course:detail:101")).thenReturn(Mono.just(1L));
when(redisUtil.delete("group_course:detail:102")).thenReturn(Mono.just(1L));
StepVerifier.create(courseLabelService.clearLabelsFromType(1L))
.verifyComplete();
verify(courseLabelRepository).clearLabelsFromType(1L);
verify(groupCourseRepository).findByCourseType(1L);
verify(redisUtil).delete("group_course:detail:101");
verify(redisUtil).delete("group_course:detail:102");
}
@Test
void clearLabelsFromType_shouldNotInvalidateCacheWhenNoCourses() {
when(courseLabelRepository.clearLabelsFromType(1L)).thenReturn(Mono.empty());
when(groupCourseRepository.findByCourseType(1L)).thenReturn(Flux.empty());
StepVerifier.create(courseLabelService.clearLabelsFromType(1L))
.verifyComplete();
verify(courseLabelRepository).clearLabelsFromType(1L);
verify(redisUtil, never()).delete(anyString());
}
// ==================== findByPage ====================
@Test
void findByPage_shouldReturnPaginatedResults() {
PageRequest pageRequest = new PageRequest();
pageRequest.setPage(0);
pageRequest.setSize(10);
CourseLabel label1 = new CourseLabel();
label1.setId(1L);
label1.setLabelName("适合新手");
CourseLabel label2 = new CourseLabel();
label2.setId(2L);
label2.setLabelName("高强度");
PageResponse<CourseLabel> pageResponse = new PageResponse<>(
List.of(label1, label2), 1, 2, 0, 10);
when(courseLabelRepository.findByPage(pageRequest)).thenReturn(Mono.just(pageResponse));
StepVerifier.create(courseLabelService.findByPage(pageRequest))
.assertNext(result -> {
assertThat(result.getContent()).hasSize(2);
assertThat(result.getTotalElements()).isEqualTo(2);
assertThat(result.getTotalPages()).isEqualTo(1);
assertThat(result.getCurrentPage()).isZero();
assertThat(result.getPageSize()).isEqualTo(10);
assertThat(result.isFirst()).isTrue();
assertThat(result.isLast()).isTrue();
})
.verifyComplete();
verify(courseLabelRepository).findByPage(pageRequest);
}
@Test
void findByPage_shouldReturnEmptyPageWhenNoResults() {
PageRequest pageRequest = new PageRequest();
PageResponse<CourseLabel> emptyPage = new PageResponse<>(
List.of(), 0, 0, 0, 10);
when(courseLabelRepository.findByPage(pageRequest)).thenReturn(Mono.just(emptyPage));
StepVerifier.create(courseLabelService.findByPage(pageRequest))
.assertNext(result -> {
assertThat(result.getContent()).isEmpty();
assertThat(result.getTotalElements()).isZero();
assertThat(result.getTotalPages()).isZero();
})
.verifyComplete();
verify(courseLabelRepository).findByPage(pageRequest);
}
}
@@ -0,0 +1,490 @@
package cn.novalon.gym.manage.groupcourse.service.impl;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseBooking;
import cn.novalon.gym.manage.groupcourse.event.BookingReminderEventPublisher;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseBookingRepository;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRepository;
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 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)
class GroupCourseBookingServiceTest {
@Mock
private IGroupCourseBookingRepository bookingRepository;
@Mock
private IGroupCourseRepository courseRepository;
@Mock
private GroupCourseRedisService redisService;
@Mock
private BookingReminderEventPublisher eventPublisher;
private GroupCourseBookingService service;
private GroupCourse testCourse;
private GroupCourseBooking testBooking;
private GroupCourseBooking savedBooking;
private static final Long COURSE_ID = 1L;
private static final Long MEMBER_ID = 100L;
private static final Long BOOKING_ID = 10L;
private static final Long OTHER_MEMBER_ID = 200L;
@BeforeEach
void setUp() {
// getCourseWithCache eagerly evaluates the switchIfEmpty argument,
// so findByIdAndDeletedAtIsNull must never return null
// Use lenient because this stub is only needed for bookCourse tests
lenient().when(courseRepository.findByIdAndDeletedAtIsNull(anyLong())).thenReturn(Mono.empty());
service = new GroupCourseBookingService(bookingRepository, courseRepository, redisService, eventPublisher);
testCourse = new GroupCourse();
testCourse.setId(COURSE_ID);
testCourse.setCourseName("瑜伽课");
testCourse.setStatus(0L);
testCourse.setStartTime(LocalDateTime.now().plusHours(2));
testCourse.setEndTime(LocalDateTime.now().plusHours(3));
testCourse.setMaxMembers(20);
testCourse.setCurrentMembers(2);
testCourse.setLocation("健身房A区");
testBooking = new GroupCourseBooking();
testBooking.setId(BOOKING_ID);
testBooking.setCourseId(COURSE_ID);
testBooking.setMemberId(MEMBER_ID);
testBooking.setStatus("0");
testBooking.setCourseName("瑜伽课");
testBooking.setCourseStartTime(LocalDateTime.now().plusHours(3));
testBooking.setCourseEndTime(LocalDateTime.now().plusHours(4));
testBooking.setLocation("健身房A区");
savedBooking = new GroupCourseBooking();
savedBooking.setId(BOOKING_ID);
savedBooking.setCourseId(COURSE_ID);
savedBooking.setMemberId(MEMBER_ID);
savedBooking.setStatus("0");
savedBooking.setCourseName("瑜伽课");
savedBooking.setCourseStartTime(LocalDateTime.now().plusHours(2));
savedBooking.setCourseEndTime(LocalDateTime.now().plusHours(3));
savedBooking.setLocation("健身房A区");
}
// ==================== bookCourse ====================
@Test
void bookCourse_shouldSucceedWithValidInputs() {
when(redisService.acquireLock(eq(COURSE_ID), anyString())).thenReturn(Mono.just(true));
when(redisService.getCachedCourse(COURSE_ID)).thenReturn(Mono.just(testCourse));
when(bookingRepository.countValidBookings(COURSE_ID)).thenReturn(Mono.just(2L));
when(bookingRepository.findConflictingBookings(eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Flux.empty());
when(bookingRepository.findValidBooking(COURSE_ID, MEMBER_ID)).thenReturn(Mono.empty());
when(bookingRepository.save(any(GroupCourseBooking.class))).thenReturn(Mono.just(savedBooking));
when(redisService.releaseLock(eq(COURSE_ID), anyString())).thenReturn(Mono.just(true));
StepVerifier.create(service.bookCourse(COURSE_ID, MEMBER_ID))
.assertNext(booking -> {
assertThat(booking.getId()).isEqualTo(BOOKING_ID);
assertThat(booking.getCourseId()).isEqualTo(COURSE_ID);
assertThat(booking.getMemberId()).isEqualTo(MEMBER_ID);
assertThat(booking.getStatus()).isEqualTo("0");
})
.verifyComplete();
verify(redisService).acquireLock(eq(COURSE_ID), anyString());
verify(redisService).getCachedCourse(COURSE_ID);
verify(bookingRepository).countValidBookings(COURSE_ID);
verify(bookingRepository).findConflictingBookings(eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class));
verify(bookingRepository).findValidBooking(COURSE_ID, MEMBER_ID);
verify(bookingRepository).save(any(GroupCourseBooking.class));
verify(redisService).releaseLock(eq(COURSE_ID), anyString());
verify(eventPublisher).publishBookingSuccessEvent(eq(BOOKING_ID), eq(MEMBER_ID), eq("瑜伽课"), anyString());
}
@Test
void bookCourse_shouldFailWhenLockNotAcquired() {
when(redisService.acquireLock(eq(COURSE_ID), anyString())).thenReturn(Mono.just(false));
StepVerifier.create(service.bookCourse(COURSE_ID, MEMBER_ID))
.expectErrorMatches(ex -> ex instanceof RuntimeException
&& "系统繁忙,请稍后重试".equals(ex.getMessage()))
.verify();
verify(redisService).acquireLock(eq(COURSE_ID), anyString());
verifyNoInteractions(bookingRepository);
verifyNoInteractions(eventPublisher);
}
@Test
void bookCourse_shouldFailWhenCourseNotFound() {
when(redisService.acquireLock(eq(COURSE_ID), anyString())).thenReturn(Mono.just(true));
when(redisService.getCachedCourse(COURSE_ID)).thenReturn(Mono.empty());
when(redisService.releaseLock(eq(COURSE_ID), anyString())).thenReturn(Mono.just(true));
StepVerifier.create(service.bookCourse(COURSE_ID, MEMBER_ID))
.expectErrorMatches(ex -> ex instanceof RuntimeException
&& "团课不存在".equals(ex.getMessage()))
.verify();
verify(redisService).acquireLock(eq(COURSE_ID), anyString());
verify(redisService).getCachedCourse(COURSE_ID);
verify(courseRepository).findByIdAndDeletedAtIsNull(COURSE_ID);
verify(redisService, atLeastOnce()).releaseLock(eq(COURSE_ID), anyString());
}
@Test
void bookCourse_shouldFailWhenCourseStatusIsCancelled() {
testCourse.setStatus(1L);
when(redisService.acquireLock(eq(COURSE_ID), anyString())).thenReturn(Mono.just(true));
when(redisService.getCachedCourse(COURSE_ID)).thenReturn(Mono.just(testCourse));
when(bookingRepository.countValidBookings(COURSE_ID)).thenReturn(Mono.just(2L));
when(redisService.releaseLock(eq(COURSE_ID), anyString())).thenReturn(Mono.just(true));
StepVerifier.create(service.bookCourse(COURSE_ID, MEMBER_ID))
.expectErrorMatches(ex -> ex instanceof RuntimeException
&& "课程已取消,无法预约".equals(ex.getMessage()))
.verify();
verify(redisService).acquireLock(eq(COURSE_ID), anyString());
verify(redisService).getCachedCourse(COURSE_ID);
verify(bookingRepository).countValidBookings(COURSE_ID);
verify(redisService, atLeastOnce()).releaseLock(eq(COURSE_ID), anyString());
verifyNoMoreInteractions(eventPublisher);
}
@Test
void bookCourse_shouldFailWhenCourseIsEnded() {
testCourse.setStatus(2L);
when(redisService.acquireLock(eq(COURSE_ID), anyString())).thenReturn(Mono.just(true));
when(redisService.getCachedCourse(COURSE_ID)).thenReturn(Mono.just(testCourse));
when(bookingRepository.countValidBookings(COURSE_ID)).thenReturn(Mono.just(2L));
when(redisService.releaseLock(eq(COURSE_ID), anyString())).thenReturn(Mono.just(true));
StepVerifier.create(service.bookCourse(COURSE_ID, MEMBER_ID))
.expectErrorMatches(ex -> ex instanceof RuntimeException
&& "课程已结束,无法预约".equals(ex.getMessage()))
.verify();
verify(redisService).acquireLock(eq(COURSE_ID), anyString());
verify(redisService).getCachedCourse(COURSE_ID);
verify(bookingRepository).countValidBookings(COURSE_ID);
verify(redisService, atLeastOnce()).releaseLock(eq(COURSE_ID), anyString());
}
@Test
void bookCourse_shouldFailWhenLessThan30minBeforeStart() {
testCourse.setStartTime(LocalDateTime.now().plusMinutes(15));
when(redisService.acquireLock(eq(COURSE_ID), anyString())).thenReturn(Mono.just(true));
when(redisService.getCachedCourse(COURSE_ID)).thenReturn(Mono.just(testCourse));
when(bookingRepository.countValidBookings(COURSE_ID)).thenReturn(Mono.just(2L));
when(redisService.releaseLock(eq(COURSE_ID), anyString())).thenReturn(Mono.just(true));
StepVerifier.create(service.bookCourse(COURSE_ID, MEMBER_ID))
.expectErrorMatches(ex -> ex instanceof RuntimeException
&& "需在课程开始前30分钟预约".equals(ex.getMessage()))
.verify();
verify(redisService).acquireLock(eq(COURSE_ID), anyString());
verify(redisService).getCachedCourse(COURSE_ID);
verify(bookingRepository).countValidBookings(COURSE_ID);
verify(redisService, atLeastOnce()).releaseLock(eq(COURSE_ID), anyString());
}
@Test
void bookCourse_shouldFailWhenCourseIsFull() {
testCourse.setCurrentMembers(20);
when(redisService.acquireLock(eq(COURSE_ID), anyString())).thenReturn(Mono.just(true));
when(redisService.getCachedCourse(COURSE_ID)).thenReturn(Mono.just(testCourse));
when(bookingRepository.countValidBookings(COURSE_ID)).thenReturn(Mono.just(20L));
when(redisService.releaseLock(eq(COURSE_ID), anyString())).thenReturn(Mono.just(true));
StepVerifier.create(service.bookCourse(COURSE_ID, MEMBER_ID))
.expectErrorMatches(ex -> ex instanceof RuntimeException
&& "课程已满".equals(ex.getMessage()))
.verify();
verify(redisService).acquireLock(eq(COURSE_ID), anyString());
verify(redisService).getCachedCourse(COURSE_ID);
verify(bookingRepository).countValidBookings(COURSE_ID);
verify(redisService, atLeastOnce()).releaseLock(eq(COURSE_ID), anyString());
}
@Test
void bookCourse_shouldFailWhenTimeConflictExists() {
GroupCourseBooking conflict = new GroupCourseBooking();
conflict.setCourseName("冲突课程");
conflict.setCourseStartTime(LocalDateTime.now().plusHours(2));
conflict.setCourseEndTime(LocalDateTime.now().plusHours(3));
when(redisService.acquireLock(eq(COURSE_ID), anyString())).thenReturn(Mono.just(true));
when(redisService.getCachedCourse(COURSE_ID)).thenReturn(Mono.just(testCourse));
when(bookingRepository.countValidBookings(COURSE_ID)).thenReturn(Mono.just(2L));
when(bookingRepository.findConflictingBookings(eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Flux.just(conflict));
when(redisService.releaseLock(eq(COURSE_ID), anyString())).thenReturn(Mono.just(true));
StepVerifier.create(service.bookCourse(COURSE_ID, MEMBER_ID))
.expectErrorMatches(ex -> ex instanceof RuntimeException
&& ex.getMessage().contains("您已预约的课程与当前课程时间冲突"))
.verify();
verify(redisService).acquireLock(eq(COURSE_ID), anyString());
verify(redisService).getCachedCourse(COURSE_ID);
verify(bookingRepository).countValidBookings(COURSE_ID);
verify(bookingRepository).findConflictingBookings(eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class));
verify(redisService, atLeastOnce()).releaseLock(eq(COURSE_ID), anyString());
}
@Test
void bookCourse_shouldFailWhenAlreadyBooked() {
when(redisService.acquireLock(eq(COURSE_ID), anyString())).thenReturn(Mono.just(true));
when(redisService.getCachedCourse(COURSE_ID)).thenReturn(Mono.just(testCourse));
when(bookingRepository.countValidBookings(COURSE_ID)).thenReturn(Mono.just(2L));
when(bookingRepository.findConflictingBookings(eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Flux.empty());
when(bookingRepository.findValidBooking(COURSE_ID, MEMBER_ID)).thenReturn(Mono.just(testBooking));
when(redisService.releaseLock(eq(COURSE_ID), anyString())).thenReturn(Mono.just(true));
StepVerifier.create(service.bookCourse(COURSE_ID, MEMBER_ID))
.expectErrorMatches(ex -> ex instanceof RuntimeException
&& "您已预约该课程".equals(ex.getMessage()))
.verify();
verify(redisService).acquireLock(eq(COURSE_ID), anyString());
verify(redisService).getCachedCourse(COURSE_ID);
verify(bookingRepository).countValidBookings(COURSE_ID);
verify(bookingRepository).findConflictingBookings(eq(MEMBER_ID), any(LocalDateTime.class), any(LocalDateTime.class));
verify(bookingRepository).findValidBooking(COURSE_ID, MEMBER_ID);
verify(redisService, atLeastOnce()).releaseLock(eq(COURSE_ID), anyString());
}
// ==================== cancelBooking ====================
@Test
void cancelBooking_shouldSucceedWithValidInputs() {
when(redisService.acquireLock(eq(BOOKING_ID), anyString())).thenReturn(Mono.just(true));
when(bookingRepository.findById(BOOKING_ID)).thenReturn(Mono.just(testBooking));
when(bookingRepository.updateStatus(BOOKING_ID, "1")).thenReturn(Mono.just(1));
when(redisService.releaseLock(eq(BOOKING_ID), anyString())).thenReturn(Mono.just(true));
when(bookingRepository.findById(BOOKING_ID)).thenReturn(Mono.just(testBooking));
StepVerifier.create(service.cancelBooking(BOOKING_ID, MEMBER_ID))
.assertNext(booking -> {
assertThat(booking.getId()).isEqualTo(BOOKING_ID);
assertThat(booking.getMemberId()).isEqualTo(MEMBER_ID);
})
.verifyComplete();
verify(redisService).acquireLock(eq(BOOKING_ID), anyString());
verify(bookingRepository, times(2)).findById(BOOKING_ID);
verify(bookingRepository).updateStatus(BOOKING_ID, "1");
verify(redisService).releaseLock(eq(BOOKING_ID), anyString());
verify(eventPublisher).publishBookingCancelEvent(eq(BOOKING_ID), eq(MEMBER_ID), eq("瑜伽课"));
}
@Test
void cancelBooking_shouldFailWhenLockNotAcquired() {
when(redisService.acquireLock(eq(BOOKING_ID), anyString())).thenReturn(Mono.just(false));
StepVerifier.create(service.cancelBooking(BOOKING_ID, MEMBER_ID))
.expectErrorMatches(ex -> ex instanceof RuntimeException
&& "系统繁忙,请稍后重试".equals(ex.getMessage()))
.verify();
verify(redisService).acquireLock(eq(BOOKING_ID), anyString());
verifyNoInteractions(eventPublisher);
}
@Test
void cancelBooking_shouldFailWhenBookingNotFound() {
when(redisService.acquireLock(eq(BOOKING_ID), anyString())).thenReturn(Mono.just(true));
when(bookingRepository.findById(BOOKING_ID)).thenReturn(Mono.empty());
when(redisService.releaseLock(eq(BOOKING_ID), anyString())).thenReturn(Mono.just(true));
StepVerifier.create(service.cancelBooking(BOOKING_ID, MEMBER_ID))
.expectErrorMatches(ex -> ex instanceof RuntimeException
&& "预约记录不存在".equals(ex.getMessage()))
.verify();
verify(redisService).acquireLock(eq(BOOKING_ID), anyString());
verify(bookingRepository).findById(BOOKING_ID);
verify(redisService, atLeastOnce()).releaseLock(eq(BOOKING_ID), anyString());
}
@Test
void cancelBooking_shouldFailWhenNotOwner() {
when(redisService.acquireLock(eq(BOOKING_ID), anyString())).thenReturn(Mono.just(true));
when(bookingRepository.findById(BOOKING_ID)).thenReturn(Mono.just(testBooking));
when(redisService.releaseLock(eq(BOOKING_ID), anyString())).thenReturn(Mono.just(true));
StepVerifier.create(service.cancelBooking(BOOKING_ID, OTHER_MEMBER_ID))
.expectErrorMatches(ex -> ex instanceof RuntimeException
&& "无权取消他人预约".equals(ex.getMessage()))
.verify();
verify(redisService).acquireLock(eq(BOOKING_ID), anyString());
verify(bookingRepository).findById(BOOKING_ID);
verify(redisService, atLeastOnce()).releaseLock(eq(BOOKING_ID), anyString());
}
@Test
void cancelBooking_shouldFailWhenAlreadyCancelled() {
testBooking.setStatus("1");
when(redisService.acquireLock(eq(BOOKING_ID), anyString())).thenReturn(Mono.just(true));
when(bookingRepository.findById(BOOKING_ID)).thenReturn(Mono.just(testBooking));
when(redisService.releaseLock(eq(BOOKING_ID), anyString())).thenReturn(Mono.just(true));
StepVerifier.create(service.cancelBooking(BOOKING_ID, MEMBER_ID))
.expectErrorMatches(ex -> ex instanceof RuntimeException
&& "预约已取消,无需重复取消".equals(ex.getMessage()))
.verify();
verify(redisService).acquireLock(eq(BOOKING_ID), anyString());
verify(bookingRepository).findById(BOOKING_ID);
verify(redisService, atLeastOnce()).releaseLock(eq(BOOKING_ID), anyString());
}
@Test
void cancelBooking_shouldFailWhenAlreadyAttended() {
testBooking.setStatus("2");
when(redisService.acquireLock(eq(BOOKING_ID), anyString())).thenReturn(Mono.just(true));
when(bookingRepository.findById(BOOKING_ID)).thenReturn(Mono.just(testBooking));
when(redisService.releaseLock(eq(BOOKING_ID), anyString())).thenReturn(Mono.just(true));
StepVerifier.create(service.cancelBooking(BOOKING_ID, MEMBER_ID))
.expectErrorMatches(ex -> ex instanceof RuntimeException
&& "课程已出席,无法取消".equals(ex.getMessage()))
.verify();
verify(redisService).acquireLock(eq(BOOKING_ID), anyString());
verify(bookingRepository).findById(BOOKING_ID);
verify(redisService, atLeastOnce()).releaseLock(eq(BOOKING_ID), anyString());
}
@Test
void cancelBooking_shouldFailWhenLessThan2hBeforeStart() {
testBooking.setCourseStartTime(LocalDateTime.now().plusHours(1));
when(redisService.acquireLock(eq(BOOKING_ID), anyString())).thenReturn(Mono.just(true));
when(bookingRepository.findById(BOOKING_ID)).thenReturn(Mono.just(testBooking));
when(redisService.releaseLock(eq(BOOKING_ID), anyString())).thenReturn(Mono.just(true));
StepVerifier.create(service.cancelBooking(BOOKING_ID, MEMBER_ID))
.expectErrorMatches(ex -> ex instanceof RuntimeException
&& "需在课程开始前2小时取消".equals(ex.getMessage()))
.verify();
verify(redisService).acquireLock(eq(BOOKING_ID), anyString());
verify(bookingRepository).findById(BOOKING_ID);
verify(redisService, atLeastOnce()).releaseLock(eq(BOOKING_ID), anyString());
}
// ==================== getBookingsByMemberId ====================
@Test
void getBookingsByMemberId_shouldReturnBookings() {
GroupCourseBooking booking1 = new GroupCourseBooking();
booking1.setId(1L);
booking1.setMemberId(MEMBER_ID);
GroupCourseBooking booking2 = new GroupCourseBooking();
booking2.setId(2L);
booking2.setMemberId(MEMBER_ID);
when(bookingRepository.findByMemberId(MEMBER_ID)).thenReturn(Flux.just(booking1, booking2));
StepVerifier.create(service.getBookingsByMemberId(MEMBER_ID))
.expectNext(booking1, booking2)
.verifyComplete();
verify(bookingRepository).findByMemberId(MEMBER_ID);
}
// ==================== getBookingById ====================
@Test
void getBookingById_shouldReturnBooking() {
when(bookingRepository.findById(BOOKING_ID)).thenReturn(Mono.just(testBooking));
StepVerifier.create(service.getBookingById(BOOKING_ID))
.assertNext(booking -> {
assertThat(booking.getId()).isEqualTo(BOOKING_ID);
assertThat(booking.getMemberId()).isEqualTo(MEMBER_ID);
})
.verifyComplete();
verify(bookingRepository).findById(BOOKING_ID);
}
// ==================== getBookingsByCourseId ====================
@Test
void getBookingsByCourseId_shouldReturnBookings() {
GroupCourseBooking booking1 = new GroupCourseBooking();
booking1.setId(1L);
booking1.setCourseId(COURSE_ID);
GroupCourseBooking booking2 = new GroupCourseBooking();
booking2.setId(2L);
booking2.setCourseId(COURSE_ID);
when(bookingRepository.findByCourseId(COURSE_ID)).thenReturn(Flux.just(booking1, booking2));
StepVerifier.create(service.getBookingsByCourseId(COURSE_ID))
.expectNext(booking1, booking2)
.verifyComplete();
verify(bookingRepository).findByCourseId(COURSE_ID);
}
// ==================== processAbsentMembers ====================
@Test
void processAbsentMembers_shouldProcessAbsentBookings() {
GroupCourseBooking absent1 = new GroupCourseBooking();
absent1.setId(1L);
absent1.setMemberId(MEMBER_ID);
absent1.setCourseId(COURSE_ID);
GroupCourseBooking absent2 = new GroupCourseBooking();
absent2.setId(2L);
absent2.setMemberId(MEMBER_ID + 1);
absent2.setCourseId(COURSE_ID);
when(bookingRepository.findAbsentMembers()).thenReturn(Flux.just(absent1, absent2));
when(bookingRepository.updateToAbsent(1L)).thenReturn(Mono.just(1));
when(bookingRepository.updateToAbsent(2L)).thenReturn(Mono.just(1));
StepVerifier.create(service.processAbsentMembers())
.assertNext(count -> assertThat(count).isEqualTo(2))
.verifyComplete();
verify(bookingRepository).findAbsentMembers();
verify(bookingRepository).updateToAbsent(1L);
verify(bookingRepository).updateToAbsent(2L);
}
@Test
void processAbsentMembers_shouldReturnZeroWhenNoAbsentBookings() {
when(bookingRepository.findAbsentMembers()).thenReturn(Flux.empty());
StepVerifier.create(service.processAbsentMembers())
.assertNext(count -> assertThat(count).isEqualTo(0))
.verifyComplete();
verify(bookingRepository).findAbsentMembers();
verify(bookingRepository, never()).updateToAbsent(anyLong());
}
}
@@ -0,0 +1,407 @@
package cn.novalon.gym.manage.groupcourse.service.impl;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseRecommend;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRecommendRepository;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class GroupCourseRecommendServiceTest {
@Mock
private IGroupCourseRecommendRepository recommendRepository;
@Mock
private IGroupCourseRepository groupCourseRepository;
private GroupCourseRecommendService service;
@Captor
private ArgumentCaptor<GroupCourseRecommend> recommendCaptor;
@BeforeEach
void setUp() {
service = new GroupCourseRecommendService(recommendRepository, groupCourseRepository);
}
private GroupCourseRecommend createRecommend(Long id, Long courseId, String title) {
GroupCourseRecommend r = new GroupCourseRecommend();
r.setId(id);
r.setCourseId(courseId);
r.setRecommendTitle(title);
r.setRecommendContent("推荐内容");
r.setRecommendReason("推荐理由");
r.setPriority(10);
r.setIsActive(true);
return r;
}
private GroupCourse createCourse(Long id, String name) {
GroupCourse c = new GroupCourse();
c.setId(id);
c.setCourseName(name);
return c;
}
// ==================== findById ====================
@Test
void findById_shouldReturnRecommendWithCourseInfo() {
GroupCourseRecommend recommend = createRecommend(1L, 10L, "本周热门");
GroupCourse course = createCourse(10L, "Push-up");
when(recommendRepository.findById(1L)).thenReturn(Mono.just(recommend));
when(groupCourseRepository.findByIdAndDeletedAtIsNull(10L)).thenReturn(Mono.just(course));
StepVerifier.create(service.findById(1L))
.assertNext(r -> {
assertThat(r.getId()).isEqualTo(1L);
assertThat(r.getRecommendTitle()).isEqualTo("本周热门");
assertThat(r.getGroupCourse()).isNotNull();
assertThat(r.getGroupCourse().getCourseName()).isEqualTo("Push-up");
})
.verifyComplete();
verify(recommendRepository).findById(1L);
verify(groupCourseRepository).findByIdAndDeletedAtIsNull(10L);
}
@Test
void findById_shouldReturnRecommendWithoutCourseWhenCourseIdIsNull() {
GroupCourseRecommend recommend = createRecommend(1L, null, "本周热门");
when(recommendRepository.findById(1L)).thenReturn(Mono.just(recommend));
StepVerifier.create(service.findById(1L))
.assertNext(r -> {
assertThat(r.getId()).isEqualTo(1L);
assertThat(r.getGroupCourse()).isNull();
})
.verifyComplete();
verify(recommendRepository).findById(1L);
verify(groupCourseRepository, never()).findByIdAndDeletedAtIsNull(any());
}
@Test
void findById_shouldReturnEmptyWhenNotFound() {
when(recommendRepository.findById(999L)).thenReturn(Mono.empty());
StepVerifier.create(service.findById(999L))
.verifyComplete();
verify(recommendRepository).findById(999L);
}
// ==================== findAll ====================
@Test
void findAll_shouldReturnAllWithCourseInfo() {
GroupCourseRecommend r1 = createRecommend(1L, 10L, "本周热门");
GroupCourseRecommend r2 = createRecommend(2L, 20L, "新课程推荐");
GroupCourse course1 = createCourse(10L, "Push-up");
GroupCourse course2 = createCourse(20L, "Yoga");
when(recommendRepository.findAll()).thenReturn(Flux.just(r1, r2));
when(groupCourseRepository.findByIdAndDeletedAtIsNull(10L)).thenReturn(Mono.just(course1));
when(groupCourseRepository.findByIdAndDeletedAtIsNull(20L)).thenReturn(Mono.just(course2));
StepVerifier.create(service.findAll())
.assertNext(r -> {
assertThat(r.getId()).isEqualTo(1L);
assertThat(r.getGroupCourse().getCourseName()).isEqualTo("Push-up");
})
.assertNext(r -> {
assertThat(r.getId()).isEqualTo(2L);
assertThat(r.getGroupCourse().getCourseName()).isEqualTo("Yoga");
})
.verifyComplete();
verify(recommendRepository).findAll();
verify(groupCourseRepository).findByIdAndDeletedAtIsNull(10L);
verify(groupCourseRepository).findByIdAndDeletedAtIsNull(20L);
}
// ==================== findAll(String, String) ====================
@Test
void findAllWithSort_shouldReturnSortedResults() {
GroupCourseRecommend r1 = createRecommend(1L, 10L, "A课程");
GroupCourseRecommend r2 = createRecommend(2L, 20L, "B课程");
GroupCourse course1 = createCourse(10L, "Push-up");
GroupCourse course2 = createCourse(20L, "Yoga");
when(recommendRepository.findAll("priority", "desc")).thenReturn(Flux.just(r1, r2));
when(groupCourseRepository.findByIdAndDeletedAtIsNull(10L)).thenReturn(Mono.just(course1));
when(groupCourseRepository.findByIdAndDeletedAtIsNull(20L)).thenReturn(Mono.just(course2));
StepVerifier.create(service.findAll("priority", "desc"))
.assertNext(r -> assertThat(r.getId()).isEqualTo(1L))
.assertNext(r -> assertThat(r.getId()).isEqualTo(2L))
.verifyComplete();
verify(recommendRepository).findAll("priority", "desc");
}
// ==================== findAllActive ====================
@Test
void findAllActive_shouldReturnOnlyActive() {
GroupCourseRecommend r1 = createRecommend(1L, 10L, "本周热门");
r1.setIsActive(true);
when(recommendRepository.findAllActive()).thenReturn(Flux.just(r1));
when(groupCourseRepository.findByIdAndDeletedAtIsNull(10L)).thenReturn(Mono.just(createCourse(10L, "Push-up")));
StepVerifier.create(service.findAllActive())
.assertNext(r -> {
assertThat(r.getId()).isEqualTo(1L);
assertThat(r.getIsActive()).isTrue();
})
.verifyComplete();
verify(recommendRepository).findAllActive();
}
// ==================== findByCourseId ====================
@Test
void findByCourseId_shouldReturnByCourseId() {
GroupCourseRecommend r1 = createRecommend(1L, 10L, "本周热门");
when(recommendRepository.findByCourseId(10L)).thenReturn(Flux.just(r1));
when(groupCourseRepository.findByIdAndDeletedAtIsNull(10L)).thenReturn(Mono.just(createCourse(10L, "Push-up")));
StepVerifier.create(service.findByCourseId(10L))
.assertNext(r -> {
assertThat(r.getCourseId()).isEqualTo(10L);
assertThat(r.getGroupCourse().getCourseName()).isEqualTo("Push-up");
})
.verifyComplete();
verify(recommendRepository).findByCourseId(10L);
}
// ==================== create ====================
@Test
void create_shouldSaveWhenCourseExists() {
GroupCourseRecommend recommend = createRecommend(null, 10L, "本周热门");
GroupCourse course = createCourse(10L, "Push-up");
GroupCourseRecommend saved = createRecommend(1L, 10L, "本周热门");
when(groupCourseRepository.findByIdAndDeletedAtIsNull(10L)).thenReturn(Mono.just(course));
when(recommendRepository.save(any(GroupCourseRecommend.class))).thenReturn(Mono.just(saved));
StepVerifier.create(service.create(recommend))
.assertNext(r -> {
assertThat(r.getId()).isEqualTo(1L);
assertThat(r.getCourseId()).isEqualTo(10L);
})
.verifyComplete();
verify(groupCourseRepository).findByIdAndDeletedAtIsNull(10L);
verify(recommendRepository).save(any(GroupCourseRecommend.class));
}
@Test
void create_shouldErrorWhenCourseIdIsNull() {
GroupCourseRecommend recommend = createRecommend(null, null, "本周热门");
StepVerifier.create(service.create(recommend))
.expectErrorMatches(e -> e instanceof RuntimeException
&& "团课ID不能为空".equals(e.getMessage()))
.verify();
verify(groupCourseRepository, never()).findByIdAndDeletedAtIsNull(any());
verify(recommendRepository, never()).save(any());
}
@Test
void create_shouldErrorWhenCourseNotFound() {
GroupCourseRecommend recommend = createRecommend(null, 999L, "本周热门");
when(groupCourseRepository.findByIdAndDeletedAtIsNull(999L)).thenReturn(Mono.empty());
StepVerifier.create(service.create(recommend))
.expectErrorMatches(e -> e instanceof RuntimeException
&& "团课不存在".equals(e.getMessage()))
.verify();
verify(groupCourseRepository).findByIdAndDeletedAtIsNull(999L);
verify(recommendRepository, never()).save(any());
}
// ==================== update ====================
@Test
void update_shouldUpdateSpecificFields() {
GroupCourseRecommend existing = createRecommend(1L, 10L, "原标题");
GroupCourseRecommend updateReq = new GroupCourseRecommend();
updateReq.setRecommendTitle("新标题");
updateReq.setRecommendContent("新内容");
updateReq.setRecommendReason("新理由");
updateReq.setPriority(20);
updateReq.setIsActive(false);
updateReq.setCourseId(30L);
GroupCourseRecommend updated = createRecommend(1L, 30L, "新标题");
updated.setRecommendContent("新内容");
updated.setRecommendReason("新理由");
updated.setPriority(20);
updated.setIsActive(false);
when(recommendRepository.findById(1L)).thenReturn(Mono.just(existing));
when(recommendRepository.update(any(GroupCourseRecommend.class))).thenReturn(Mono.just(updated));
StepVerifier.create(service.update(1L, updateReq))
.assertNext(r -> {
assertThat(r.getRecommendTitle()).isEqualTo("新标题");
assertThat(r.getRecommendContent()).isEqualTo("新内容");
})
.verifyComplete();
verify(recommendRepository).findById(1L);
verify(recommendRepository).update(recommendCaptor.capture());
GroupCourseRecommend captured = recommendCaptor.getValue();
assertThat(captured.getRecommendTitle()).isEqualTo("新标题");
assertThat(captured.getRecommendContent()).isEqualTo("新内容");
assertThat(captured.getRecommendReason()).isEqualTo("新理由");
assertThat(captured.getPriority()).isEqualTo(20);
assertThat(captured.getIsActive()).isFalse();
assertThat(captured.getCourseId()).isEqualTo(30L);
}
@Test
void update_shouldErrorWhenNotFound() {
GroupCourseRecommend updateReq = new GroupCourseRecommend();
updateReq.setRecommendTitle("新标题");
when(recommendRepository.findById(999L)).thenReturn(Mono.empty());
StepVerifier.create(service.update(999L, updateReq))
.expectErrorMatches(e -> e instanceof RuntimeException
&& "团课推荐不存在".equals(e.getMessage()))
.verify();
verify(recommendRepository).findById(999L);
verify(recommendRepository, never()).update(any());
}
@Test
void update_shouldOnlyUpdateNonNullFields() {
GroupCourseRecommend existing = createRecommend(1L, 10L, "原标题");
GroupCourseRecommend updateReq = new GroupCourseRecommend();
updateReq.setRecommendTitle("仅标题");
when(recommendRepository.findById(1L)).thenReturn(Mono.just(existing));
when(recommendRepository.update(any(GroupCourseRecommend.class))).thenReturn(Mono.just(existing));
StepVerifier.create(service.update(1L, updateReq))
.assertNext(r -> assertThat(r.getRecommendTitle()).isEqualTo("仅标题"))
.verifyComplete();
verify(recommendRepository).update(recommendCaptor.capture());
GroupCourseRecommend captured = recommendCaptor.getValue();
assertThat(captured.getRecommendTitle()).isEqualTo("仅标题");
assertThat(captured.getRecommendContent()).isEqualTo("推荐内容");
assertThat(captured.getRecommendReason()).isEqualTo("推荐理由");
assertThat(captured.getPriority()).isEqualTo(10);
assertThat(captured.getIsActive()).isTrue();
assertThat(captured.getCourseId()).isEqualTo(10L);
}
// ==================== delete ====================
@Test
void delete_shouldDeleteWhenExists() {
GroupCourseRecommend recommend = createRecommend(1L, 10L, "本周热门");
when(recommendRepository.findById(1L)).thenReturn(Mono.just(recommend));
when(recommendRepository.deleteById(1L)).thenReturn(Mono.empty());
StepVerifier.create(service.delete(1L))
.verifyComplete();
verify(recommendRepository).findById(1L);
verify(recommendRepository).deleteById(1L);
}
@Test
void delete_shouldErrorWhenNotFound() {
when(recommendRepository.findById(999L)).thenReturn(Mono.empty());
StepVerifier.create(service.delete(999L))
.expectErrorMatches(e -> e instanceof RuntimeException
&& "团课推荐不存在".equals(e.getMessage()))
.verify();
verify(recommendRepository).findById(999L);
verify(recommendRepository, never()).deleteById(any());
}
// ==================== enable ====================
@Test
void enable_shouldUpdateActiveStatusToTrue() {
GroupCourseRecommend result = createRecommend(1L, 10L, "本周热门");
result.setIsActive(true);
when(recommendRepository.updateActiveStatus(1L, true)).thenReturn(Mono.just(result));
StepVerifier.create(service.enable(1L))
.assertNext(r -> {
assertThat(r.getId()).isEqualTo(1L);
assertThat(r.getIsActive()).isTrue();
})
.verifyComplete();
verify(recommendRepository).updateActiveStatus(1L, true);
}
@Test
void enable_shouldErrorWhenNotFound() {
when(recommendRepository.updateActiveStatus(999L, true)).thenReturn(Mono.empty());
StepVerifier.create(service.enable(999L))
.expectErrorMatches(e -> e instanceof RuntimeException
&& "团课推荐不存在".equals(e.getMessage()))
.verify();
verify(recommendRepository).updateActiveStatus(999L, true);
}
// ==================== disable ====================
@Test
void disable_shouldUpdateActiveStatusToFalse() {
GroupCourseRecommend result = createRecommend(1L, 10L, "本周热门");
result.setIsActive(false);
when(recommendRepository.updateActiveStatus(1L, false)).thenReturn(Mono.just(result));
StepVerifier.create(service.disable(1L))
.assertNext(r -> {
assertThat(r.getId()).isEqualTo(1L);
assertThat(r.getIsActive()).isFalse();
})
.verifyComplete();
verify(recommendRepository).updateActiveStatus(1L, false);
}
@Test
void disable_shouldErrorWhenNotFound() {
when(recommendRepository.updateActiveStatus(999L, false)).thenReturn(Mono.empty());
StepVerifier.create(service.disable(999L))
.expectErrorMatches(e -> e instanceof RuntimeException
&& "团课推荐不存在".equals(e.getMessage()))
.verify();
verify(recommendRepository).updateActiveStatus(999L, false);
}
}
@@ -0,0 +1,167 @@
package cn.novalon.gym.manage.groupcourse.service.impl;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import com.fasterxml.jackson.databind.ObjectMapper;
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.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.ReactiveValueOperations;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.time.Duration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class GroupCourseRedisServiceTest {
@Mock
private ReactiveRedisTemplate<String, Object> reactiveRedisTemplate;
@Mock
private ReactiveValueOperations<String, Object> reactiveValueOps;
private ObjectMapper objectMapper = new ObjectMapper();
private GroupCourseRedisService service;
private GroupCourse testCourse;
@BeforeEach
void setUp() {
lenient().when(reactiveRedisTemplate.opsForValue()).thenReturn(reactiveValueOps);
service = new GroupCourseRedisService(reactiveRedisTemplate, objectMapper);
testCourse = new GroupCourse();
testCourse.setId(1L);
testCourse.setCourseName("瑜伽课");
testCourse.setCourseType(1L);
}
// ==================== cacheCourse ====================
@Test
void cacheCourse_shouldSerializeAndSetInRedis() {
when(reactiveValueOps.set(eq("group_course:1"), anyString(), eq(Duration.ofMinutes(5))))
.thenReturn(Mono.just(true));
StepVerifier.create(service.cacheCourse(testCourse))
.verifyComplete();
verify(reactiveValueOps).set(eq("group_course:1"), anyString(), eq(Duration.ofMinutes(5)));
}
@Test
void cacheCourse_shouldErrorWhenRedisFails() {
when(reactiveValueOps.set(eq("group_course:1"), anyString(), eq(Duration.ofMinutes(5))))
.thenReturn(Mono.error(new RuntimeException("Redis error")));
StepVerifier.create(service.cacheCourse(testCourse))
.expectError(RuntimeException.class)
.verify();
}
// ==================== getCachedCourse ====================
@Test
void getCachedCourse_shouldReturnCachedCourseWhenFound() throws Exception {
String json = objectMapper.writeValueAsString(testCourse);
when(reactiveValueOps.get("group_course:1")).thenReturn(Mono.just(json));
StepVerifier.create(service.getCachedCourse(1L))
.assertNext(course -> {
assertThat(course.getId()).isEqualTo(1L);
assertThat(course.getCourseName()).isEqualTo("瑜伽课");
})
.verifyComplete();
}
@Test
void getCachedCourse_shouldReturnEmptyWhenCacheMiss() {
when(reactiveValueOps.get("group_course:1")).thenReturn(Mono.empty());
StepVerifier.create(service.getCachedCourse(1L))
.verifyComplete();
}
@Test
void getCachedCourse_shouldReturnEmptyWhenDeserializationFails() {
String invalidJson = "invalid json";
when(reactiveValueOps.get("group_course:1")).thenReturn(Mono.just(invalidJson));
StepVerifier.create(service.getCachedCourse(1L))
.verifyComplete();
}
// ==================== invalidateCourseCache ====================
@Test
void invalidateCourseCache_shouldDeleteFromRedis() {
when(reactiveRedisTemplate.delete("group_course:1")).thenReturn(Mono.just(1L));
StepVerifier.create(service.invalidateCourseCache(1L))
.verifyComplete();
verify(reactiveRedisTemplate).delete("group_course:1");
}
// ==================== acquireLock ====================
@Test
void acquireLock_shouldReturnTrueWhenLockAcquired() {
when(reactiveValueOps.setIfAbsent("booking_lock:1", "req-1", Duration.ofSeconds(30)))
.thenReturn(Mono.just(true));
StepVerifier.create(service.acquireLock(1L, "req-1"))
.assertNext(acquired -> assertThat(acquired).isTrue())
.verifyComplete();
}
@Test
void acquireLock_shouldReturnFalseWhenLockNotAcquired() {
when(reactiveValueOps.setIfAbsent("booking_lock:1", "req-1", Duration.ofSeconds(30)))
.thenReturn(Mono.just(false));
StepVerifier.create(service.acquireLock(1L, "req-1"))
.assertNext(acquired -> assertThat(acquired).isFalse())
.verifyComplete();
}
// ==================== releaseLock ====================
@Test
void releaseLock_shouldReturnTrueWhenRequestIdMatches() {
when(reactiveValueOps.get("booking_lock:1")).thenReturn(Mono.just("req-1"));
when(reactiveRedisTemplate.delete("booking_lock:1")).thenReturn(Mono.just(1L));
StepVerifier.create(service.releaseLock(1L, "req-1"))
.assertNext(released -> assertThat(released).isTrue())
.verifyComplete();
}
@Test
void releaseLock_shouldReturnFalseWhenRequestIdDoesNotMatch() {
when(reactiveValueOps.get("booking_lock:1")).thenReturn(Mono.just("req-2"));
StepVerifier.create(service.releaseLock(1L, "req-1"))
.assertNext(released -> assertThat(released).isFalse())
.verifyComplete();
verify(reactiveRedisTemplate, never()).delete(anyString());
}
@Test
void releaseLock_shouldReturnFalseWhenKeyNotFound() {
when(reactiveValueOps.get("booking_lock:1")).thenReturn(Mono.empty());
StepVerifier.create(service.releaseLock(1L, "req-1"))
.assertNext(released -> assertThat(released).isFalse())
.verifyComplete();
}
}
@@ -0,0 +1,838 @@
package cn.novalon.gym.manage.groupcourse.service.impl;
import cn.novalon.gym.manage.common.dto.PageRequest;
import cn.novalon.gym.manage.common.dto.PageResponse;
import cn.novalon.gym.manage.common.util.RedisUtil;
import cn.novalon.gym.manage.file.core.domain.SysFile;
import cn.novalon.gym.manage.file.core.service.ISysFileService;
import cn.novalon.gym.manage.groupcourse.domain.CourseLabel;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseBooking;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseDetail;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseType;
import cn.novalon.gym.manage.groupcourse.dto.GroupCourseQueryDto;
import cn.novalon.gym.manage.groupcourse.enums.CourseEvent;
import cn.novalon.gym.manage.groupcourse.enums.CourseStatus;
import cn.novalon.gym.manage.groupcourse.handler.GroupCourseStateMachine;
import cn.novalon.gym.manage.groupcourse.repository.ICourseLabelRepository;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseBookingRepository;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRepository;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseTypeRepository;
import cn.novalon.gym.manage.groupcourse.util.QRCodeUtil;
import cn.novalon.gym.manage.member.entity.MemberCard;
import cn.novalon.gym.manage.member.entity.MemberCardRecord;
import cn.novalon.gym.manage.member.enums.MemberCardType;
import cn.novalon.gym.manage.member.repository.MemberCardRepository;
import cn.novalon.gym.manage.member.service.IMemberCardRecordService;
import cn.novalon.gym.manage.sys.core.domain.SysUser;
import cn.novalon.gym.manage.sys.core.repository.ISysUserRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
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.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.r2dbc.core.DatabaseClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.math.BigDecimal;
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 GroupCourseServiceTest {
@Mock
private IGroupCourseRepository groupCourseRepository;
@Mock
private IGroupCourseBookingRepository bookingRepository;
@Mock
private IGroupCourseTypeRepository groupCourseTypeRepository;
@Mock
private ICourseLabelRepository courseLabelRepository;
@Mock
private IMemberCardRecordService memberCardRecordService;
@Mock
private MemberCardRepository memberCardRepository;
@Mock
private RedisUtil redisUtil;
@Mock
private GroupCourseStateMachine stateMachine;
@Mock
private DatabaseClient databaseClient;
@Mock
private ISysFileService fileService;
@Mock
private ISysUserRepository sysUserRepository;
private final ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new JavaTimeModule());
private GroupCourseService groupCourseService;
private GroupCourse testCourse;
private GroupCourseType testType;
private CourseLabel testLabel;
private GroupCourseBooking testBooking;
private SysUser testCoach;
private SysFile testSysFile;
@BeforeEach
void setUp() {
groupCourseService = new GroupCourseService(
groupCourseRepository, bookingRepository, groupCourseTypeRepository,
courseLabelRepository, memberCardRecordService, memberCardRepository,
redisUtil, objectMapper, stateMachine, databaseClient, fileService, sysUserRepository
);
testCourse = new GroupCourse();
testCourse.setId(1L);
testCourse.setCourseName("瑜伽入门课");
testCourse.setCoachId(10L);
testCourse.setCourseType(100L);
testCourse.setStartTime(LocalDateTime.now().plusDays(7));
testCourse.setEndTime(LocalDateTime.now().plusDays(7).plusHours(1));
testCourse.setMaxMembers(20);
testCourse.setCurrentMembers(5);
testCourse.setStatus(CourseStatus.NORMAL.getValue());
testCourse.setLocation("A区教室");
testCourse.setDescription("适合初学者的瑜伽课程");
testCourse.setStoredValueAmount(new BigDecimal("50.00"));
testType = new GroupCourseType();
testType.setId(100L);
testType.setTypeName("瑜伽入门");
testType.setCategory("柔韧与平衡类");
testType.setBaseDifficulty(2);
testLabel = new CourseLabel();
testLabel.setId(1000L);
testLabel.setLabelName("适合新手");
testLabel.setColor("#52c41a");
testBooking = new GroupCourseBooking();
testBooking.setId(500L);
testBooking.setCourseId(1L);
testBooking.setMemberId(200L);
testBooking.setMemberCardRecordId(300L);
testBooking.setStatus("0");
testCoach = new SysUser();
testCoach.setId(10L);
testCoach.setNickname("张教练");
testCoach.setUsername("coach_zhang");
testSysFile = new SysFile();
testSysFile.setId(999L);
testSysFile.setFileName("qrcode_1.png");
}
private void mockClearCache() {
when(redisUtil.deleteByPattern("group_course:page:*")).thenReturn(Mono.just(0L));
when(redisUtil.deleteByPattern("group_course:id:*")).thenReturn(Mono.just(0L));
when(redisUtil.deleteByPattern("group_course:detail:*")).thenReturn(Mono.just(0L));
when(redisUtil.deleteByPattern("datacount:statistics:*")).thenReturn(Mono.just(0L));
}
private void mockEnrichCurrentMembers() {
when(bookingRepository.countValidBookings(anyLong())).thenReturn(Mono.just(5L));
}
// ==================== findDetailById ====================
@Test
void findDetailById_cacheHit_shouldReturnDetail() throws Exception {
String cacheKey = "group_course:detail:1";
GroupCourseDetail detail = new GroupCourseDetail();
detail.setId(1L);
detail.setCourseName("瑜伽入门课");
detail.setCoachName("张教练");
String json = objectMapper.writeValueAsString(detail);
when(redisUtil.get(cacheKey, String.class)).thenReturn(Mono.just(json));
// switchIfEmpty 的 Mono 参数被急切求值,需要 mock 以避免 NPE
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.empty());
StepVerifier.create(groupCourseService.findDetailById(1L))
.assertNext(result -> {
assertThat(result.getId()).isEqualTo(1L);
assertThat(result.getCourseName()).isEqualTo("瑜伽入门课");
assertThat(result.getCoachName()).isEqualTo("张教练");
})
.verifyComplete();
verify(redisUtil).get(cacheKey, String.class);
// switchIfEmpty 的 Mono 参数被急切求值,findByIdAndDeletedAtIsNull 会被调用但不会订阅
}
@Test
void findDetailById_cacheMissWithType_shouldBuildAndCacheDetail() throws Exception {
String cacheKey = "group_course:detail:1";
when(redisUtil.get(cacheKey, String.class)).thenReturn(Mono.empty());
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.just(testCourse));
when(groupCourseTypeRepository.findById(100L)).thenReturn(Mono.just(testType));
when(courseLabelRepository.findByTypeId(100L)).thenReturn(Flux.just(testLabel));
when(sysUserRepository.findByIdIncludingDeleted(10L)).thenReturn(Mono.just(testCoach));
when(bookingRepository.countValidBookings(1L)).thenReturn(Mono.just(8L));
when(redisUtil.setWithExpire(eq(cacheKey), anyString(), eq(300L))).thenReturn(Mono.just(true));
StepVerifier.create(groupCourseService.findDetailById(1L))
.assertNext(result -> {
assertThat(result.getId()).isEqualTo(1L);
assertThat(result.getCourseName()).isEqualTo("瑜伽入门课");
assertThat(result.getCoachName()).isEqualTo("张教练");
assertThat(result.getCurrentMembers()).isEqualTo(8);
assertThat(result.getTypeInfo()).isNotNull();
assertThat(result.getTypeInfo().getTypeName()).isEqualTo("瑜伽入门");
assertThat(result.getLabels()).hasSize(1);
assertThat(result.getLabels().get(0).getLabelName()).isEqualTo("适合新手");
})
.verifyComplete();
verify(redisUtil).get(cacheKey, String.class);
verify(groupCourseRepository).findByIdAndDeletedAtIsNull(1L);
verify(groupCourseTypeRepository).findById(100L);
verify(courseLabelRepository).findByTypeId(100L);
verify(sysUserRepository).findByIdIncludingDeleted(10L);
verify(bookingRepository).countValidBookings(1L);
verify(redisUtil).setWithExpire(eq(cacheKey), anyString(), eq(300L));
}
@Test
void findDetailById_cacheMissNullCourseType_shouldBuildDetailWithoutType() {
String cacheKey = "group_course:detail:1";
testCourse.setCourseType(null);
when(redisUtil.get(cacheKey, String.class)).thenReturn(Mono.empty());
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.just(testCourse));
when(sysUserRepository.findByIdIncludingDeleted(10L)).thenReturn(Mono.just(testCoach));
when(bookingRepository.countValidBookings(1L)).thenReturn(Mono.just(3L));
when(redisUtil.setWithExpire(eq(cacheKey), anyString(), eq(300L))).thenReturn(Mono.just(true));
StepVerifier.create(groupCourseService.findDetailById(1L))
.assertNext(result -> {
assertThat(result.getId()).isEqualTo(1L);
assertThat(result.getCourseName()).isEqualTo("瑜伽入门课");
assertThat(result.getTypeInfo()).isNull();
assertThat(result.getCoachName()).isEqualTo("张教练");
assertThat(result.getCurrentMembers()).isEqualTo(3);
})
.verifyComplete();
verify(groupCourseTypeRepository, never()).findById(anyLong());
verify(courseLabelRepository, never()).findByTypeId(anyLong());
}
@Test
void findDetailById_cacheMissNotFound_shouldReturnEmpty() {
String cacheKey = "group_course:detail:1";
when(redisUtil.get(cacheKey, String.class)).thenReturn(Mono.empty());
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.empty());
StepVerifier.create(groupCourseService.findDetailById(1L))
.verifyComplete();
verify(groupCourseRepository).findByIdAndDeletedAtIsNull(1L);
verifyNoInteractions(groupCourseTypeRepository, courseLabelRepository, sysUserRepository, bookingRepository);
}
@Test
void findDetailById_cacheParseError_shouldDeleteCacheAndFallback() throws Exception {
String cacheKey = "group_course:detail:1";
when(redisUtil.get(cacheKey, String.class)).thenReturn(Mono.just("invalid-json"));
when(redisUtil.delete(cacheKey)).thenReturn(Mono.just(1L));
// After cache delete, switchIfEmpty kicks in
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.just(testCourse));
when(groupCourseTypeRepository.findById(100L)).thenReturn(Mono.just(testType));
when(courseLabelRepository.findByTypeId(100L)).thenReturn(Flux.just(testLabel));
when(sysUserRepository.findByIdIncludingDeleted(10L)).thenReturn(Mono.just(testCoach));
when(bookingRepository.countValidBookings(1L)).thenReturn(Mono.just(5L));
when(redisUtil.setWithExpire(eq(cacheKey), anyString(), eq(300L))).thenReturn(Mono.just(true));
StepVerifier.create(groupCourseService.findDetailById(1L))
.assertNext(result -> {
assertThat(result.getId()).isEqualTo(1L);
assertThat(result.getCourseName()).isEqualTo("瑜伽入门课");
})
.verifyComplete();
verify(redisUtil).delete(cacheKey);
verify(groupCourseRepository).findByIdAndDeletedAtIsNull(1L);
}
// ==================== findById ====================
@Test
void findById_cacheHit_shouldReturnCourse() throws Exception {
String cacheKey = "group_course:id:1";
String json = objectMapper.writeValueAsString(testCourse);
when(redisUtil.get(cacheKey, String.class)).thenReturn(Mono.just(json));
// switchIfEmpty 的 Mono 参数被急切求值,需要 mock 以避免 NPE
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.empty());
when(bookingRepository.countValidBookings(1L)).thenReturn(Mono.just(5L));
StepVerifier.create(groupCourseService.findById(1L))
.assertNext(result -> {
assertThat(result.getId()).isEqualTo(1L);
assertThat(result.getCourseName()).isEqualTo("瑜伽入门课");
})
.verifyComplete();
verify(redisUtil).get(cacheKey, String.class);
verify(bookingRepository).countValidBookings(1L);
// switchIfEmpty 的 Mono 参数被急切求值,findByIdAndDeletedAtIsNull 会被调用但不会订阅
}
@Test
void findById_cacheMiss_shouldQueryDbAndCache() throws Exception {
String cacheKey = "group_course:id:1";
when(redisUtil.get(cacheKey, String.class)).thenReturn(Mono.empty());
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.just(testCourse));
when(redisUtil.setWithExpire(eq(cacheKey), anyString(), eq(300L))).thenReturn(Mono.just(true));
when(bookingRepository.countValidBookings(1L)).thenReturn(Mono.just(5L));
StepVerifier.create(groupCourseService.findById(1L))
.assertNext(result -> {
assertThat(result.getId()).isEqualTo(1L);
assertThat(result.getCourseName()).isEqualTo("瑜伽入门课");
})
.verifyComplete();
verify(groupCourseRepository).findByIdAndDeletedAtIsNull(1L);
verify(redisUtil).setWithExpire(eq(cacheKey), anyString(), eq(300L));
verify(bookingRepository).countValidBookings(1L);
}
@Test
void findById_cacheMissNotFound_shouldReturnEmpty() {
String cacheKey = "group_course:id:1";
when(redisUtil.get(cacheKey, String.class)).thenReturn(Mono.empty());
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.empty());
StepVerifier.create(groupCourseService.findById(1L))
.verifyComplete();
verify(groupCourseRepository).findByIdAndDeletedAtIsNull(1L);
}
// ==================== findAll ====================
@Test
void findAll_shouldReturnAllCourses() {
GroupCourse course2 = new GroupCourse();
course2.setId(2L);
course2.setCourseName("力量训练课");
when(groupCourseRepository.findAll()).thenReturn(Flux.just(testCourse, course2));
StepVerifier.create(groupCourseService.findAll())
.expectNext(testCourse, course2)
.verifyComplete();
verify(groupCourseRepository).findAll();
}
@Test
void findAll_withIncludeDeletedTrue_shouldReturnAll() {
when(groupCourseRepository.findAll()).thenReturn(Flux.just(testCourse));
StepVerifier.create(groupCourseService.findAll(true))
.expectNext(testCourse)
.verifyComplete();
verify(groupCourseRepository).findAll();
}
@Test
void findAll_withIncludeDeletedFalse_shouldReturnNonDeleted() {
when(groupCourseRepository.findByDeletedAtIsNull()).thenReturn(Flux.just(testCourse));
StepVerifier.create(groupCourseService.findAll(false))
.expectNext(testCourse)
.verifyComplete();
verify(groupCourseRepository).findByDeletedAtIsNull();
}
// ==================== findByPage ====================
@Test
void findByPage_cacheHit_shouldReturnPage() throws Exception {
PageRequest pageRequest = new PageRequest();
pageRequest.setPage(0);
pageRequest.setSize(10);
PageResponse<GroupCourse> pageResponse = new PageResponse<>(
List.of(testCourse), 1, 1, 0, 10
);
String cacheKey = "group_course:page:0:10:false:id:asc::";
String json = objectMapper.writeValueAsString(pageResponse);
when(redisUtil.get(cacheKey, String.class)).thenReturn(Mono.just(json));
StepVerifier.create(groupCourseService.findByPage(pageRequest, false))
.assertNext(result -> {
assertThat(result.getContent()).hasSize(1);
assertThat(result.getContent().get(0).getCourseName()).isEqualTo("瑜伽入门课");
assertThat(result.getTotalElements()).isEqualTo(1);
})
.verifyComplete();
verify(redisUtil).get(cacheKey, String.class);
verifyNoInteractions(groupCourseRepository);
}
@Test
void findByPage_cacheMiss_shouldQueryDbAndCache() throws Exception {
PageRequest pageRequest = new PageRequest();
pageRequest.setPage(0);
pageRequest.setSize(10);
PageResponse<GroupCourse> pageResponse = new PageResponse<>(
List.of(testCourse), 1, 1, 0, 10
);
String cacheKey = "group_course:page:0:10:false:id:asc::";
when(redisUtil.get(cacheKey, String.class)).thenReturn(Mono.empty());
when(groupCourseRepository.findByPageAndNotDeleted(pageRequest)).thenReturn(Mono.just(pageResponse));
when(redisUtil.setWithExpire(eq(cacheKey), anyString(), eq(300L))).thenReturn(Mono.just(true));
StepVerifier.create(groupCourseService.findByPage(pageRequest, false))
.assertNext(result -> {
assertThat(result.getContent()).hasSize(1);
assertThat(result.getTotalElements()).isEqualTo(1);
})
.verifyComplete();
verify(groupCourseRepository).findByPageAndNotDeleted(pageRequest);
verify(redisUtil).setWithExpire(eq(cacheKey), anyString(), eq(300L));
}
// ==================== create ====================
@Test
void create_shouldSaveCourseAndGenerateQrCode() {
mockClearCache();
when(groupCourseRepository.save(any(GroupCourse.class))).thenReturn(Mono.just(testCourse));
try (MockedStatic<QRCodeUtil> qrUtil = mockStatic(QRCodeUtil.class)) {
qrUtil.when(() -> QRCodeUtil.generateQrCodeBytes(anyString())).thenReturn(new byte[]{1, 2, 3});
when(fileService.saveBytes(any(byte[].class), eq("qrcode_1.png"), eq("image/png"), eq("system")))
.thenReturn(Mono.just(testSysFile));
when(groupCourseRepository.update(any(GroupCourse.class))).thenReturn(Mono.just(testCourse));
StepVerifier.create(groupCourseService.create(testCourse))
.assertNext(result -> {
assertThat(result.getId()).isEqualTo(1L);
assertThat(result.getCourseName()).isEqualTo("瑜伽入门课");
})
.verifyComplete();
verify(groupCourseRepository).save(testCourse);
verify(fileService).saveBytes(any(byte[].class), eq("qrcode_1.png"), eq("image/png"), eq("system"));
verify(groupCourseRepository).update(any(GroupCourse.class));
}
}
@Test
void create_qrCodeFails_shouldStillSaveCourse() {
when(groupCourseRepository.save(any(GroupCourse.class))).thenReturn(Mono.just(testCourse));
try (MockedStatic<QRCodeUtil> qrUtil = mockStatic(QRCodeUtil.class)) {
qrUtil.when(() -> QRCodeUtil.generateQrCodeBytes(anyString()))
.thenThrow(new RuntimeException("QR generation failed"));
StepVerifier.create(groupCourseService.create(testCourse))
.expectError(RuntimeException.class)
.verify();
verify(groupCourseRepository).save(testCourse);
verify(fileService, never()).saveBytes(any(), anyString(), anyString(), anyString());
}
}
// ==================== update ====================
@Test
void update_shouldUpdateExistingCourse() {
mockClearCache();
GroupCourse updateData = new GroupCourse();
updateData.setCourseName("瑜伽进阶课");
updateData.setLocation("B区教室");
updateData.setMaxMembers(30);
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.just(testCourse));
when(groupCourseRepository.update(any(GroupCourse.class))).thenReturn(Mono.just(testCourse));
StepVerifier.create(groupCourseService.update(1L, updateData))
.assertNext(result -> {
assertThat(result.getCourseName()).isEqualTo("瑜伽进阶课");
assertThat(result.getLocation()).isEqualTo("B区教室");
assertThat(result.getMaxMembers()).isEqualTo(30);
})
.verifyComplete();
verify(groupCourseRepository).findByIdAndDeletedAtIsNull(1L);
verify(groupCourseRepository).update(argThat(course ->
"瑜伽进阶课".equals(course.getCourseName()) &&
"B区教室".equals(course.getLocation()) &&
course.getMaxMembers() == 30
));
}
@Test
void update_courseNotFound_shouldError() {
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.empty());
StepVerifier.create(groupCourseService.update(1L, new GroupCourse()))
.expectErrorMatches(ex -> ex instanceof RuntimeException && "团课不存在".equals(ex.getMessage()))
.verify();
verify(groupCourseRepository).findByIdAndDeletedAtIsNull(1L);
verify(groupCourseRepository, never()).update(any());
}
// ==================== cancel ====================
@Test
void cancel_shouldCancelAndRefundMembers() {
mockClearCache();
MemberCardRecord record = MemberCardRecord.builder()
.id(300L)
.memberCardId(400L)
.remainingAmount(200.0)
.build();
MemberCard card = MemberCard.builder()
.memberCardId(400L)
.memberCardType(MemberCardType.STORED_VALUE_CARD.name())
.build();
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.just(testCourse));
when(stateMachine.validateTransition(any(GroupCourse.class), eq(CourseEvent.CANCEL))).thenReturn(Mono.empty());
when(groupCourseRepository.cancel(1L)).thenReturn(Mono.just(testCourse));
// refundBookingMembers flow
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.just(testCourse));
when(bookingRepository.findByCourseId(1L)).thenReturn(Flux.just(testBooking));
when(memberCardRecordService.findById(300L)).thenReturn(Mono.just(record));
when(memberCardRepository.findById(400L)).thenReturn(Mono.just(card));
when(memberCardRecordService.renewCard(300L, 0, 50.0, record.getExpireTime())).thenReturn(Mono.just(1));
when(bookingRepository.updateStatus(500L, "1")).thenReturn(Mono.just(1));
testCourse.setStatus(CourseStatus.CANCELLED.getValue());
StepVerifier.create(groupCourseService.cancel(1L))
.assertNext(result -> {
assertThat(result.getId()).isEqualTo(1L);
})
.verifyComplete();
verify(stateMachine).validateTransition(any(GroupCourse.class), eq(CourseEvent.CANCEL));
verify(groupCourseRepository).cancel(1L);
verify(memberCardRecordService).renewCard(300L, 0, 50.0, record.getExpireTime());
verify(bookingRepository).updateStatus(500L, "1");
}
@Test
void cancel_courseNotFound_shouldError() {
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.empty());
StepVerifier.create(groupCourseService.cancel(1L))
.expectErrorMatches(ex -> ex instanceof RuntimeException && "团课不存在".equals(ex.getMessage()))
.verify();
}
@Test
void cancel_within24Hours_shouldError() {
testCourse.setStartTime(LocalDateTime.now().plusHours(1));
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.just(testCourse));
when(stateMachine.validateTransition(any(GroupCourse.class), eq(CourseEvent.CANCEL))).thenReturn(Mono.empty());
StepVerifier.create(groupCourseService.cancel(1L))
.expectErrorMatches(ex -> ex instanceof RuntimeException && "课程取消需提前24小时".equals(ex.getMessage()))
.verify();
verify(groupCourseRepository, never()).cancel(anyLong());
}
@Test
void cancel_stateMachineRejects_shouldError() {
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.just(testCourse));
when(stateMachine.validateTransition(any(GroupCourse.class), eq(CourseEvent.CANCEL)))
.thenReturn(Mono.error(new IllegalStateException("不允许的状态转换")));
StepVerifier.create(groupCourseService.cancel(1L))
.expectError(IllegalStateException.class)
.verify();
verify(groupCourseRepository, never()).cancel(anyLong());
}
// ==================== signIn ====================
@Test
void signIn_shouldSucceed() {
mockClearCache();
GroupCourse signingCourse = new GroupCourse();
signingCourse.setId(1L);
signingCourse.setCourseName("瑜伽入门课");
signingCourse.setCoachId(10L);
signingCourse.setCourseType(100L);
signingCourse.setStartTime(LocalDateTime.now().plusHours(1));
signingCourse.setEndTime(LocalDateTime.now().plusHours(2));
signingCourse.setMaxMembers(20);
signingCourse.setCurrentMembers(5);
signingCourse.setStatus(CourseStatus.NORMAL.getValue());
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.just(signingCourse));
when(bookingRepository.countValidBookings(1L)).thenReturn(Mono.just(5L));
when(bookingRepository.findValidBooking(1L, 200L)).thenReturn(Mono.just(testBooking));
when(bookingRepository.updateStatus(500L, "2")).thenReturn(Mono.just(1));
StepVerifier.create(groupCourseService.signIn(1L, 200L))
.assertNext(result -> {
assertThat(result.getId()).isEqualTo(1L);
})
.verifyComplete();
verify(bookingRepository).findValidBooking(1L, 200L);
verify(bookingRepository).updateStatus(500L, "2");
}
@Test
void signIn_courseCancelled_shouldError() {
GroupCourse cancelledCourse = new GroupCourse();
cancelledCourse.setId(1L);
cancelledCourse.setStartTime(LocalDateTime.now().plusHours(1));
cancelledCourse.setEndTime(LocalDateTime.now().plusHours(2));
cancelledCourse.setMaxMembers(20);
cancelledCourse.setCurrentMembers(5);
cancelledCourse.setStatus(CourseStatus.CANCELLED.getValue());
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.just(cancelledCourse));
when(bookingRepository.countValidBookings(1L)).thenReturn(Mono.just(5L));
StepVerifier.create(groupCourseService.signIn(1L, 200L))
.expectErrorMatches(ex -> ex instanceof RuntimeException && "团课已取消,无法签到".equals(ex.getMessage()))
.verify();
}
@Test
void signIn_notInSignInWindow_shouldError() {
GroupCourse futureCourse = new GroupCourse();
futureCourse.setId(1L);
futureCourse.setStartTime(LocalDateTime.now().plusDays(1));
futureCourse.setEndTime(LocalDateTime.now().plusDays(1).plusHours(1));
futureCourse.setMaxMembers(20);
futureCourse.setCurrentMembers(5);
futureCourse.setStatus(CourseStatus.NORMAL.getValue());
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.just(futureCourse));
when(bookingRepository.countValidBookings(1L)).thenReturn(Mono.just(5L));
StepVerifier.create(groupCourseService.signIn(1L, 200L))
.expectErrorMatches(ex -> ex instanceof RuntimeException && ex.getMessage().startsWith("未到签到时间"))
.verify();
}
@Test
void signIn_courseFull_shouldError() {
GroupCourse fullCourse = new GroupCourse();
fullCourse.setId(1L);
fullCourse.setStartTime(LocalDateTime.now().plusHours(1));
fullCourse.setEndTime(LocalDateTime.now().plusHours(2));
fullCourse.setMaxMembers(20);
fullCourse.setCurrentMembers(20);
fullCourse.setStatus(CourseStatus.NORMAL.getValue());
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.just(fullCourse));
when(bookingRepository.countValidBookings(1L)).thenReturn(Mono.just(20L));
StepVerifier.create(groupCourseService.signIn(1L, 200L))
.expectErrorMatches(ex -> ex instanceof RuntimeException && "课程已满员,无法签到".equals(ex.getMessage()))
.verify();
}
@Test
void signIn_notBooked_shouldError() {
GroupCourse signingCourse = new GroupCourse();
signingCourse.setId(1L);
signingCourse.setStartTime(LocalDateTime.now().plusHours(1));
signingCourse.setEndTime(LocalDateTime.now().plusHours(2));
signingCourse.setMaxMembers(20);
signingCourse.setCurrentMembers(5);
signingCourse.setStatus(CourseStatus.NORMAL.getValue());
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.just(signingCourse));
when(bookingRepository.countValidBookings(1L)).thenReturn(Mono.just(5L));
when(bookingRepository.findValidBooking(1L, 200L)).thenReturn(Mono.empty());
StepVerifier.create(groupCourseService.signIn(1L, 200L))
.expectErrorMatches(ex -> ex instanceof RuntimeException && "您未预约此团课".equals(ex.getMessage()))
.verify();
}
// ==================== delete ====================
@Test
void delete_cancelledCourse_shouldSucceed() {
mockClearCache();
testCourse.setStatus(CourseStatus.CANCELLED.getValue());
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.just(testCourse));
when(groupCourseRepository.deleteById(1L)).thenReturn(Mono.empty());
StepVerifier.create(groupCourseService.delete(1L))
.verifyComplete();
verify(groupCourseRepository).deleteById(1L);
}
@Test
void delete_endedCourse_shouldSucceed() {
mockClearCache();
testCourse.setStatus(CourseStatus.ENDED.getValue());
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.just(testCourse));
when(groupCourseRepository.deleteById(1L)).thenReturn(Mono.empty());
StepVerifier.create(groupCourseService.delete(1L))
.verifyComplete();
verify(groupCourseRepository).deleteById(1L);
}
@Test
void delete_activeCourse_shouldError() {
testCourse.setStatus(CourseStatus.NORMAL.getValue());
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.just(testCourse));
StepVerifier.create(groupCourseService.delete(1L))
.expectErrorMatches(ex -> ex instanceof RuntimeException && ex.getMessage().startsWith("只有已取消或已结束的课程才能删除"))
.verify();
verify(groupCourseRepository, never()).deleteById(anyLong());
}
@Test
void delete_courseNotFound_shouldError() {
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.empty());
StepVerifier.create(groupCourseService.delete(1L))
.expectErrorMatches(ex -> ex instanceof RuntimeException && "团课不存在".equals(ex.getMessage()))
.verify();
}
// ==================== checkCoachConflict ====================
@Test
void checkCoachConflict_noConflict_shouldReturnEmpty() {
LocalDateTime startTime = LocalDateTime.now().plusDays(1);
LocalDateTime endTime = startTime.plusHours(1);
GroupCourse existing = new GroupCourse();
existing.setId(2L);
existing.setCoachId(10L);
existing.setStartTime(startTime.plusDays(2));
existing.setEndTime(startTime.plusDays(2).plusHours(1));
existing.setStatus(CourseStatus.NORMAL.getValue());
when(groupCourseRepository.findByCoachId(10L)).thenReturn(Flux.just(existing));
StepVerifier.create(groupCourseService.checkCoachConflict(10L, startTime, endTime, null))
.assertNext(result -> assertThat(result).isEmpty())
.verifyComplete();
}
@Test
void checkCoachConflict_hasConflict_shouldReturnConflictingCourses() {
LocalDateTime startTime = LocalDateTime.now().plusDays(1);
LocalDateTime endTime = startTime.plusHours(1);
GroupCourse conflicting = new GroupCourse();
conflicting.setId(2L);
conflicting.setCoachId(10L);
conflicting.setStartTime(startTime);
conflicting.setEndTime(endTime.plusMinutes(30));
conflicting.setStatus(CourseStatus.NORMAL.getValue());
when(groupCourseRepository.findByCoachId(10L)).thenReturn(Flux.just(conflicting));
StepVerifier.create(groupCourseService.checkCoachConflict(10L, startTime, endTime, null))
.assertNext(result -> {
assertThat(result).hasSize(1);
assertThat(result.get(0).getId()).isEqualTo(2L);
})
.verifyComplete();
}
@Test
void checkCoachConflict_excludeSelf_shouldNotReturnExcludedCourse() {
LocalDateTime startTime = LocalDateTime.now().plusDays(1);
LocalDateTime endTime = startTime.plusHours(1);
GroupCourse self = new GroupCourse();
self.setId(1L);
self.setCoachId(10L);
self.setStartTime(startTime);
self.setEndTime(endTime.plusMinutes(30));
self.setStatus(CourseStatus.NORMAL.getValue());
when(groupCourseRepository.findByCoachId(10L)).thenReturn(Flux.just(self));
StepVerifier.create(groupCourseService.checkCoachConflict(10L, startTime, endTime, 1L))
.assertNext(result -> assertThat(result).isEmpty())
.verifyComplete();
}
@Test
void checkCoachConflict_nullParams_shouldReturnEmpty() {
StepVerifier.create(groupCourseService.checkCoachConflict(null, LocalDateTime.now(), LocalDateTime.now().plusHours(1), null))
.assertNext(result -> assertThat(result).isEmpty())
.verifyComplete();
verifyNoInteractions(groupCourseRepository);
}
// ==================== searchGroupCourses ====================
@Test
void searchGroupCourses_shouldDelegateToRepository() {
GroupCourseQueryDto query = new GroupCourseQueryDto();
query.setCourseName("瑜伽");
PageResponse<GroupCourse> pageResponse = new PageResponse<>(List.of(testCourse), 1, 1, 0, 10);
when(groupCourseRepository.searchGroupCourses(query)).thenReturn(Mono.just(pageResponse));
StepVerifier.create(groupCourseService.searchGroupCourses(query))
.assertNext(result -> {
assertThat(result.getContent()).hasSize(1);
assertThat(result.getContent().get(0).getCourseName()).isEqualTo("瑜伽入门课");
})
.verifyComplete();
verify(groupCourseRepository).searchGroupCourses(query);
}
}
@@ -0,0 +1,297 @@
package cn.novalon.gym.manage.groupcourse.service.impl;
import cn.novalon.gym.manage.common.dto.PageRequest;
import cn.novalon.gym.manage.common.dto.PageResponse;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseType;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRepository;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseTypeRepository;
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 reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class GroupCourseTypeServiceTest {
@Mock
private IGroupCourseTypeRepository groupCourseTypeRepository;
@Mock
private IGroupCourseRepository groupCourseRepository;
private GroupCourseTypeService groupCourseTypeService;
private GroupCourseType yogaType;
private GroupCourseType strengthType;
private GroupCourseType pilatesType;
@BeforeEach
void setUp() {
groupCourseTypeService = new GroupCourseTypeService(groupCourseTypeRepository, groupCourseRepository);
yogaType = new GroupCourseType();
yogaType.setId(1L);
yogaType.setTypeName("瑜伽入门");
yogaType.setBaseDifficulty(2);
yogaType.setDescription("适合初学者的瑜伽课程");
yogaType.setCategory("柔韧与平衡类");
strengthType = new GroupCourseType();
strengthType.setId(2L);
strengthType.setTypeName("力量训练");
strengthType.setBaseDifficulty(5);
strengthType.setDescription("增强肌肉力量");
strengthType.setCategory("力量类");
pilatesType = new GroupCourseType();
pilatesType.setId(3L);
pilatesType.setTypeName("普拉提进阶");
pilatesType.setBaseDifficulty(4);
pilatesType.setDescription("核心力量训练");
pilatesType.setCategory("柔韧与平衡类");
}
// ==================== findById ====================
@Test
void findById_shouldReturnType() {
when(groupCourseTypeRepository.findById(1L)).thenReturn(Mono.just(yogaType));
StepVerifier.create(groupCourseTypeService.findById(1L))
.expectNext(yogaType)
.verifyComplete();
verify(groupCourseTypeRepository).findById(1L);
}
@Test
void findById_shouldReturnEmptyWhenNotFound() {
when(groupCourseTypeRepository.findById(999L)).thenReturn(Mono.empty());
StepVerifier.create(groupCourseTypeService.findById(999L))
.expectNextCount(0)
.verifyComplete();
verify(groupCourseTypeRepository).findById(999L);
}
// ==================== findAll (no param) ====================
@Test
void findAll_shouldReturnAll() {
when(groupCourseTypeRepository.findAll(false)).thenReturn(Flux.just(yogaType, strengthType, pilatesType));
StepVerifier.create(groupCourseTypeService.findAll())
.expectNext(yogaType, strengthType, pilatesType)
.verifyComplete();
verify(groupCourseTypeRepository).findAll(false);
}
// ==================== findAll (boolean) ====================
@Test
void findAll_withDeletedFlag_shouldReturnAllWithDeleted() {
when(groupCourseTypeRepository.findAll(true)).thenReturn(Flux.just(yogaType, strengthType));
StepVerifier.create(groupCourseTypeService.findAll(true))
.expectNext(yogaType, strengthType)
.verifyComplete();
verify(groupCourseTypeRepository).findAll(true);
}
// ==================== findByKeyword ====================
@Test
void findByKeyword_shouldReturnMatchingTypes() {
when(groupCourseTypeRepository.findByKeyword("瑜伽")).thenReturn(Flux.just(yogaType));
StepVerifier.create(groupCourseTypeService.findByKeyword("瑜伽"))
.expectNext(yogaType)
.verifyComplete();
verify(groupCourseTypeRepository).findByKeyword("瑜伽");
}
// ==================== findByCategory ====================
@Test
void findByCategory_shouldReturnTypesInCategory() {
when(groupCourseTypeRepository.findByCategory("柔韧与平衡类")).thenReturn(Flux.just(yogaType, pilatesType));
StepVerifier.create(groupCourseTypeService.findByCategory("柔韧与平衡类"))
.expectNext(yogaType, pilatesType)
.verifyComplete();
verify(groupCourseTypeRepository).findByCategory("柔韧与平衡类");
}
// ==================== findByCategoryAndKeyword ====================
@Test
void findByCategoryAndKeyword_shouldFilterByBoth() {
when(groupCourseTypeRepository.findByCategoryAndKeyword("柔韧与平衡类", "瑜伽"))
.thenReturn(Flux.just(yogaType));
StepVerifier.create(groupCourseTypeService.findByCategoryAndKeyword("柔韧与平衡类", "瑜伽"))
.expectNext(yogaType)
.verifyComplete();
verify(groupCourseTypeRepository).findByCategoryAndKeyword("柔韧与平衡类", "瑜伽");
}
// ==================== create ====================
@Test
void create_shouldSaveWhenNameNotExists() {
GroupCourseType newType = new GroupCourseType();
newType.setTypeName("HIIT训练");
newType.setBaseDifficulty(6);
newType.setCategory("有氧类");
when(groupCourseTypeRepository.findByTypeName("HIIT训练")).thenReturn(Mono.empty());
when(groupCourseTypeRepository.save(any(GroupCourseType.class))).thenReturn(Mono.just(newType));
StepVerifier.create(groupCourseTypeService.create(newType))
.expectNext(newType)
.verifyComplete();
verify(groupCourseTypeRepository).findByTypeName("HIIT训练");
verify(groupCourseTypeRepository).save(any(GroupCourseType.class));
}
@Test
void create_shouldErrorWhenNameAlreadyExists() {
GroupCourseType duplicate = new GroupCourseType();
duplicate.setTypeName("瑜伽入门");
when(groupCourseTypeRepository.findByTypeName("瑜伽入门")).thenReturn(Mono.just(yogaType));
// switchIfEmpty 是急切求值的,需要为 save 提供 stub 避免 NPE
lenient().when(groupCourseTypeRepository.save(any(GroupCourseType.class))).thenReturn(Mono.just(new GroupCourseType()));
StepVerifier.create(groupCourseTypeService.create(duplicate))
.expectErrorMatches(ex -> ex instanceof RuntimeException
&& "团课类型名称已存在".equals(ex.getMessage()))
.verify();
verify(groupCourseTypeRepository).findByTypeName("瑜伽入门");
}
// ==================== update ====================
@Test
void update_shouldUpdateType() {
GroupCourseType update = new GroupCourseType();
update.setId(1L);
update.setTypeName("瑜伽进阶");
update.setBaseDifficulty(4);
update.setCategory("柔韧与平衡类");
when(groupCourseTypeRepository.update(any(GroupCourseType.class))).thenReturn(Mono.just(update));
StepVerifier.create(groupCourseTypeService.update(1L, update))
.expectNext(update)
.verifyComplete();
verify(groupCourseTypeRepository).update(any(GroupCourseType.class));
}
// ==================== delete ====================
@Test
void delete_shouldDeleteWhenNoDependentCourses() {
when(groupCourseRepository.findByCourseType(1L)).thenReturn(Flux.empty());
when(groupCourseTypeRepository.deleteById(1L)).thenReturn(Mono.empty());
StepVerifier.create(groupCourseTypeService.delete(1L))
.verifyComplete();
verify(groupCourseRepository).findByCourseType(1L);
verify(groupCourseTypeRepository).deleteById(1L);
}
@Test
void delete_shouldErrorWhenDependentCoursesExist() {
GroupCourse dependentCourse = new GroupCourse();
dependentCourse.setId(100L);
dependentCourse.setCourseName("瑜伽入门课");
when(groupCourseRepository.findByCourseType(1L)).thenReturn(Flux.just(dependentCourse));
StepVerifier.create(groupCourseTypeService.delete(1L))
.expectErrorMatches(ex -> ex instanceof RuntimeException
&& "该类型下存在团课,无法删除".equals(ex.getMessage()))
.verify();
verify(groupCourseRepository).findByCourseType(1L);
}
// ==================== findCategories ====================
@Test
void findCategories_shouldReturnDistinctCategories() {
when(groupCourseTypeRepository.findAll(false)).thenReturn(Flux.just(yogaType, strengthType, pilatesType));
StepVerifier.create(groupCourseTypeService.findCategories())
.expectNext("柔韧与平衡类", "力量类")
.verifyComplete();
verify(groupCourseTypeRepository).findAll(false);
}
@Test
void findCategories_shouldFilterEmptyCategories() {
GroupCourseType emptyCategory = new GroupCourseType();
emptyCategory.setId(4L);
emptyCategory.setTypeName("空分类");
emptyCategory.setCategory("");
when(groupCourseTypeRepository.findAll(false)).thenReturn(Flux.just(yogaType, emptyCategory));
StepVerifier.create(groupCourseTypeService.findCategories())
.expectNext("柔韧与平衡类")
.verifyComplete();
verify(groupCourseTypeRepository).findAll(false);
}
// ==================== findByPage ====================
@Test
void findByPage_shouldReturnPaginatedResults() {
PageRequest pageRequest = new PageRequest();
pageRequest.setPage(0);
pageRequest.setSize(10);
PageResponse<GroupCourseType> pageResponse = new PageResponse<>(
List.of(yogaType, strengthType),
1, 2, 0, 10
);
when(groupCourseTypeRepository.findByPage(pageRequest)).thenReturn(Mono.just(pageResponse));
StepVerifier.create(groupCourseTypeService.findByPage(pageRequest))
.expectNextMatches(response ->
response.getContent().size() == 2 &&
response.getTotalElements() == 2 &&
response.getTotalPages() == 1 &&
response.getCurrentPage() == 0
)
.verifyComplete();
verify(groupCourseTypeRepository).findByPage(pageRequest);
}
}