From 86b7555943bfd2c936400eee1b89125e43e3f794 Mon Sep 17 00:00:00 2001 From: liwentao Date: Thu, 23 Jul 2026 20:16:31 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84e2e=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E4=B8=8E=E5=90=8E=E7=AB=AF=E6=B5=8B=E8=AF=95=EF=BC=8C=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=E5=B0=8F=E7=A8=8B=E5=BA=8F=E7=AB=AFUI=E5=B1=82?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E6=9A=82=E6=9C=AA=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/handler/PhoneAuthHandlerTest.java | 175 +++++++ .../checkin/handler/CheckInHandlerTest.java | 172 +++++++ .../coach/handler/CoachHandlerTest.java | 176 +++++++ .../handler/DataStatisticsHandlerTest.java | 219 +++++++++ .../handler/GroupCourseHandlerTest.java | 227 +++++++++ .../manage/file/core/domain/SysFileTest.java | 57 +++ .../service/impl/BannerServiceImplTest.java | 186 ++++++++ .../impl/SysNoticeServiceImplTest.java | 170 +++++++ .../impl/SysUserMessageServiceImplTest.java | 195 ++++++++ .../notify/handler/BannerHandlerTest.java | 219 +++++++++ .../handler/SysUserMessageHandlerTest.java | 228 +++++++++ .../e2e/helpers/api-helper.js | 51 ++ .../coach-brand-notify-violation.spec.js | 170 +++++++ gym-manage-uniapp/e2e/helpers/api-helper.js | 93 ++++ .../e2e/specs/member-advanced-search.spec.js | 194 ++++++++ .../e2e/specs/member-brand-notify.spec.js | 179 +++++++ .../brand-management-workflow.spec.ts | 267 +++++++++++ gym-manage-web/playwright/.auth/user.json | 4 +- .../src/__tests__/api/banner.api.test.ts | 64 +++ .../src/__tests__/api/coach.api.test.ts | 71 +++ .../src/__tests__/api/memberCard.api.test.ts | 76 +++ .../components/BannerManagement.test.ts | 371 +++++++++++++++ .../components/CoachManagement.test.ts | 392 ++++++++++++++++ .../components/CourseLabelManagement.test.ts | 247 ++++++++++ .../CourseRecommendManagement.test.ts | 251 ++++++++++ .../components/CourseTypeManagement.test.ts | 269 +++++++++++ .../__tests__/components/Forbidden.test.ts | 119 +++++ .../components/GroupCourseManagement.test.ts | 443 ++++++++++++++++++ .../components/MemberCardManagement.test.ts | 353 ++++++++++++++ .../components/MemberManagement.test.ts | 372 +++++++++++++++ .../components/StatisticsDashboard.test.ts | 386 +++++++++++++++ .../src/__tests__/constants/status.test.ts | 128 +++++ .../src/__tests__/utils/dateFormat.test.ts | 88 ++++ .../src/__tests__/utils/permission.test.ts | 95 ++++ .../src/__tests__/utils/signature.test.ts | 103 ++++ gym-manage-web/vitest.config.ts | 8 + test-context-ui.json | 6 +- 37 files changed, 6819 insertions(+), 5 deletions(-) create mode 100644 gym-manage-api/gym-auth/src/test/java/cn/novalon/gym/manage/auth/handler/PhoneAuthHandlerTest.java create mode 100644 gym-manage-api/gym-checkIn/src/test/java/cn/novalon/gym/manage/checkin/handler/CheckInHandlerTest.java create mode 100644 gym-manage-api/gym-coach/src/test/java/cn/novalon/gym/manage/coach/handler/CoachHandlerTest.java create mode 100644 gym-manage-api/gym-dataCount/src/test/java/cn/novalon/gym/manage/datacount/handler/DataStatisticsHandlerTest.java create mode 100644 gym-manage-api/gym-groupCourse/src/test/java/cn/novalon/gym/manage/groupcourse/handler/GroupCourseHandlerTest.java create mode 100644 gym-manage-api/manage-file/src/test/java/cn/novalon/gym/manage/file/core/domain/SysFileTest.java create mode 100644 gym-manage-api/manage-notify/src/test/java/cn/novalon/gym/manage/notify/core/service/impl/BannerServiceImplTest.java create mode 100644 gym-manage-api/manage-notify/src/test/java/cn/novalon/gym/manage/notify/core/service/impl/SysNoticeServiceImplTest.java create mode 100644 gym-manage-api/manage-notify/src/test/java/cn/novalon/gym/manage/notify/core/service/impl/SysUserMessageServiceImplTest.java create mode 100644 gym-manage-api/manage-notify/src/test/java/cn/novalon/gym/manage/notify/handler/BannerHandlerTest.java create mode 100644 gym-manage-api/manage-notify/src/test/java/cn/novalon/gym/manage/notify/handler/SysUserMessageHandlerTest.java create mode 100644 gym-manage-coach-uniapp/e2e/specs/coach-brand-notify-violation.spec.js create mode 100644 gym-manage-uniapp/e2e/specs/member-advanced-search.spec.js create mode 100644 gym-manage-uniapp/e2e/specs/member-brand-notify.spec.js create mode 100644 gym-manage-web/e2e/journeys/brand-management-workflow.spec.ts create mode 100644 gym-manage-web/src/__tests__/api/banner.api.test.ts create mode 100644 gym-manage-web/src/__tests__/api/coach.api.test.ts create mode 100644 gym-manage-web/src/__tests__/api/memberCard.api.test.ts create mode 100644 gym-manage-web/src/__tests__/components/BannerManagement.test.ts create mode 100644 gym-manage-web/src/__tests__/components/CoachManagement.test.ts create mode 100644 gym-manage-web/src/__tests__/components/CourseLabelManagement.test.ts create mode 100644 gym-manage-web/src/__tests__/components/CourseRecommendManagement.test.ts create mode 100644 gym-manage-web/src/__tests__/components/CourseTypeManagement.test.ts create mode 100644 gym-manage-web/src/__tests__/components/Forbidden.test.ts create mode 100644 gym-manage-web/src/__tests__/components/GroupCourseManagement.test.ts create mode 100644 gym-manage-web/src/__tests__/components/MemberCardManagement.test.ts create mode 100644 gym-manage-web/src/__tests__/components/MemberManagement.test.ts create mode 100644 gym-manage-web/src/__tests__/components/StatisticsDashboard.test.ts create mode 100644 gym-manage-web/src/__tests__/constants/status.test.ts create mode 100644 gym-manage-web/src/__tests__/utils/dateFormat.test.ts create mode 100644 gym-manage-web/src/__tests__/utils/permission.test.ts create mode 100644 gym-manage-web/src/__tests__/utils/signature.test.ts diff --git a/gym-manage-api/gym-auth/src/test/java/cn/novalon/gym/manage/auth/handler/PhoneAuthHandlerTest.java b/gym-manage-api/gym-auth/src/test/java/cn/novalon/gym/manage/auth/handler/PhoneAuthHandlerTest.java new file mode 100644 index 0000000..9edef3f --- /dev/null +++ b/gym-manage-api/gym-auth/src/test/java/cn/novalon/gym/manage/auth/handler/PhoneAuthHandlerTest.java @@ -0,0 +1,175 @@ +package cn.novalon.gym.manage.auth.handler; + +import cn.novalon.gym.manage.auth.dto.PhoneLoginDto; +import cn.novalon.gym.manage.auth.dto.PhoneCodeLoginDto; +import cn.novalon.gym.manage.auth.dto.SendCodeRequest; +import cn.novalon.gym.manage.auth.service.PhoneAuthService; +import cn.novalon.gym.manage.auth.vo.PhoneLoginVO; +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.Mono; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class PhoneAuthHandlerTest { + + @Mock + private PhoneAuthService phoneAuthService; + + private PhoneAuthHandler phoneAuthHandler; + + @BeforeEach + void setUp() { + phoneAuthHandler = new PhoneAuthHandler(phoneAuthService); + } + + // ==================== oneClickLogin ==================== + + @Test + void oneClickLogin_shouldReturnOkWithLoginResult() { + PhoneLoginVO loginVO = new PhoneLoginVO(); + loginVO.setAccessToken("test-jwt-token"); + loginVO.setPhone("13800138000"); + + PhoneLoginDto dto = new PhoneLoginDto(); + dto.setAccessToken("dcloud-access-token"); + + when(phoneAuthService.oneClickLogin(any(PhoneLoginDto.class))).thenReturn(Mono.just(loginVO)); + + MockServerRequest request = MockServerRequest.builder() + .body(Mono.just(dto)); + + Mono result = phoneAuthHandler.oneClickLogin(request); + + StepVerifier.create(result) + .assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK)) + .verifyComplete(); + + verify(phoneAuthService).oneClickLogin(any(PhoneLoginDto.class)); + } + + @Test + void oneClickLogin_shouldPropagateServiceError() { + PhoneLoginDto dto = new PhoneLoginDto(); + dto.setAccessToken("invalid-token"); + + when(phoneAuthService.oneClickLogin(any(PhoneLoginDto.class))) + .thenReturn(Mono.error(new RuntimeException("Auth failed"))); + + MockServerRequest request = MockServerRequest.builder() + .body(Mono.just(dto)); + + Mono result = phoneAuthHandler.oneClickLogin(request); + + StepVerifier.create(result) + .expectError(RuntimeException.class) + .verify(); + } + + // ==================== sendSmsCode ==================== + + @Test + void sendSmsCode_shouldReturnOkWithSuccessTrue() { + SendCodeRequest sendCodeRequest = new SendCodeRequest(); + sendCodeRequest.setPhone("13800138000"); + + when(phoneAuthService.sendSmsCode("13800138000")).thenReturn(Mono.just(true)); + + MockServerRequest request = MockServerRequest.builder() + .body(Mono.just(sendCodeRequest)); + + Mono result = phoneAuthHandler.sendSmsCode(request); + + StepVerifier.create(result) + .assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK)) + .verifyComplete(); + } + + @Test + void sendSmsCode_shouldReturnOkWithSuccessFalseWhenServiceReturnsFalse() { + SendCodeRequest sendCodeRequest = new SendCodeRequest(); + sendCodeRequest.setPhone("13800138000"); + + when(phoneAuthService.sendSmsCode("13800138000")).thenReturn(Mono.just(false)); + + MockServerRequest request = MockServerRequest.builder() + .body(Mono.just(sendCodeRequest)); + + Mono result = phoneAuthHandler.sendSmsCode(request); + + StepVerifier.create(result) + .assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK)) + .verifyComplete(); + } + + @Test + void sendSmsCode_shouldPropagateError() { + SendCodeRequest sendCodeRequest = new SendCodeRequest(); + sendCodeRequest.setPhone("13800138000"); + + when(phoneAuthService.sendSmsCode("13800138000")) + .thenReturn(Mono.error(new RuntimeException("SMS service unavailable"))); + + MockServerRequest request = MockServerRequest.builder() + .body(Mono.just(sendCodeRequest)); + + Mono result = phoneAuthHandler.sendSmsCode(request); + + StepVerifier.create(result) + .expectError(RuntimeException.class) + .verify(); + } + + // ==================== codeLogin ==================== + + @Test + void codeLogin_shouldReturnOkWithLoginResult() { + PhoneLoginVO loginVO = new PhoneLoginVO(); + loginVO.setAccessToken("test-jwt-token"); + + PhoneCodeLoginDto dto = new PhoneCodeLoginDto(); + dto.setPhone("13800138000"); + dto.setCode("123456"); + + when(phoneAuthService.codeLogin(any(PhoneCodeLoginDto.class))).thenReturn(Mono.just(loginVO)); + + MockServerRequest request = MockServerRequest.builder() + .body(Mono.just(dto)); + + Mono result = phoneAuthHandler.codeLogin(request); + + StepVerifier.create(result) + .assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK)) + .verifyComplete(); + } + + @Test + void codeLogin_shouldPropagateServiceError() { + PhoneCodeLoginDto dto = new PhoneCodeLoginDto(); + dto.setPhone("13800138000"); + dto.setCode("wrong-code"); + + when(phoneAuthService.codeLogin(any(PhoneCodeLoginDto.class))) + .thenReturn(Mono.error(new RuntimeException("Invalid code"))); + + MockServerRequest request = MockServerRequest.builder() + .body(Mono.just(dto)); + + Mono result = phoneAuthHandler.codeLogin(request); + + StepVerifier.create(result) + .expectError(RuntimeException.class) + .verify(); + } +} diff --git a/gym-manage-api/gym-checkIn/src/test/java/cn/novalon/gym/manage/checkin/handler/CheckInHandlerTest.java b/gym-manage-api/gym-checkIn/src/test/java/cn/novalon/gym/manage/checkin/handler/CheckInHandlerTest.java new file mode 100644 index 0000000..4b53f9f --- /dev/null +++ b/gym-manage-api/gym-checkIn/src/test/java/cn/novalon/gym/manage/checkin/handler/CheckInHandlerTest.java @@ -0,0 +1,172 @@ +package cn.novalon.gym.manage.checkin.handler; + +import cn.novalon.gym.manage.checkIn.handler.CheckInHandler; +import cn.novalon.gym.manage.checkIn.service.impl.CheckServiceImpl; +import cn.novalon.gym.manage.checkIn.vo.QRCodeVo; +import cn.novalon.gym.manage.checkIn.vo.SignInRecordVO; +import cn.novalon.gym.manage.checkIn.vo.SignInStatsVO; +import cn.novalon.gym.manage.sys.util.AuthUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.reactive.function.server.MockServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.LocalDate; +import java.time.LocalDateTime; +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 CheckInHandlerTest { + + @Mock + private AuthUtil authUtil; + + @Mock + private CheckServiceImpl checkService; + + private CheckInHandler checkInHandler; + + private static final Long MEMBER_ID = 10001L; + + @BeforeEach + void setUp() { + checkInHandler = new CheckInHandler(authUtil, checkService); + } + + // ==================== checkIn ==================== + + @Test + void checkIn_shouldReturnOk() { + Map body = Map.of("qrContent", "checkin:member:10001"); + + when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID); + when(checkService.checkIn(MEMBER_ID, "checkin:member:10001")).thenReturn(Mono.just("签到成功")); + + MockServerRequest request = MockServerRequest.builder() + .body(Mono.just(body)); + + Mono result = checkInHandler.checkIn(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + + verify(checkService).checkIn(MEMBER_ID, "checkin:member:10001"); + } + + @Test + void checkIn_shouldReturnBadRequestOnError() { + Map body = Map.of("qrContent", "invalid-content"); + + when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID); + when(checkService.checkIn(MEMBER_ID, "invalid-content")) + .thenReturn(Mono.error(new RuntimeException("Invalid QR code"))); + + MockServerRequest request = MockServerRequest.builder() + .body(Mono.just(body)); + + Mono result = checkInHandler.checkIn(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + // ==================== getQRCode ==================== + + @Test + void getQRCode_shouldReturnOkWithQRCode() { + QRCodeVo qrCode = new QRCodeVo("base64content", false, "qr-content", 200, 200, LocalDate.now()); + + when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID); + when(checkService.getQRCode(MEMBER_ID)).thenReturn(Mono.just(qrCode)); + + MockServerRequest request = MockServerRequest.builder().build(); + + Mono result = checkInHandler.getQRCode(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + } + + // ==================== getSignInRecords ==================== + + @Test + void getSignInRecords_shouldReturnOkWithRecords() { + List records = List.of(createTestRecord()); + + when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID); + when(checkService.getSignInRecords(eq(MEMBER_ID), any(LocalDate.class), any(LocalDate.class))) + .thenReturn(Flux.fromIterable(records)); + + MockServerRequest request = MockServerRequest.builder() + .queryParam("startDate", "2025-01-01") + .queryParam("endDate", "2025-01-31") + .build(); + + Mono result = checkInHandler.getSignInRecords(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + } + + // ==================== getSignInStatistics ==================== + + @Test + void getSignInStatistics_shouldReturnOkWithStats() { + SignInStatsVO stats = new SignInStatsVO(); + + when(authUtil.getMemberIdOrThrow(any())).thenReturn(MEMBER_ID); + when(checkService.getSignInStats(eq(MEMBER_ID), any(LocalDate.class), any(LocalDate.class))) + .thenReturn(Mono.just(stats)); + + MockServerRequest request = MockServerRequest.builder() + .queryParam("startDate", "2025-01-01") + .queryParam("endDate", "2025-01-31") + .build(); + + Mono result = checkInHandler.getSignInStatistics(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + } + + // ==================== getDailySignInStats ==================== + + @Test + void getDailySignInStats_shouldReturnOkWithDailyStats() { + SignInStatsVO stats = new SignInStatsVO(); + + when(checkService.getDailySignInStats(any(LocalDate.class))).thenReturn(Mono.just(stats)); + + MockServerRequest request = MockServerRequest.builder() + .queryParam("date", "2025-01-15") + .build(); + + Mono result = checkInHandler.getDailySignInStats(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + } + + // ==================== helper ==================== + + private SignInRecordVO createTestRecord() { + SignInRecordVO record = new SignInRecordVO(); + record.setId(1L); + record.setMemberId(MEMBER_ID); + record.setSignInTime(LocalDateTime.now()); + record.setSignInType("QR_CODE"); + record.setSignInStatus("SUCCESS"); + return record; + } +} diff --git a/gym-manage-api/gym-coach/src/test/java/cn/novalon/gym/manage/coach/handler/CoachHandlerTest.java b/gym-manage-api/gym-coach/src/test/java/cn/novalon/gym/manage/coach/handler/CoachHandlerTest.java new file mode 100644 index 0000000..170b67c --- /dev/null +++ b/gym-manage-api/gym-coach/src/test/java/cn/novalon/gym/manage/coach/handler/CoachHandlerTest.java @@ -0,0 +1,176 @@ +package cn.novalon.gym.manage.coach.handler; + +import cn.novalon.gym.manage.coach.service.CoachCourseService; +import cn.novalon.gym.manage.sys.core.domain.SysUser; +import cn.novalon.gym.manage.groupcourse.domain.GroupCourse; + +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 jakarta.validation.Validator; + +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 CoachHandlerTest { + + @Mock + private CoachCourseService coachCourseService; + + @Mock + private Validator validator; + + private CoachHandler coachHandler; + + @BeforeEach + void setUp() { + coachHandler = new CoachHandler(coachCourseService, validator); + } + + // ==================== getAllCoaches ==================== + + @Test + void getAllCoaches_shouldReturnOkWithCoachList() { + SysUser coach = mock(SysUser.class); + when(coachCourseService.getAllCoaches()).thenReturn(Flux.just(coach)); + + MockServerRequest request = MockServerRequest.builder().build(); + + Mono result = coachHandler.getAllCoaches(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + + verify(coachCourseService).getAllCoaches(); + } + + @Test + void getAllCoaches_shouldReturnOkWhenEmptyList() { + when(coachCourseService.getAllCoaches()).thenReturn(Flux.empty()); + + MockServerRequest request = MockServerRequest.builder().build(); + + Mono result = coachHandler.getAllCoaches(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + } + + // ==================== getCoachCourses ==================== + + @Test + void getCoachCourses_shouldReturnOkWithCourses() { + GroupCourse course = mock(GroupCourse.class); + when(coachCourseService.getCoachCourses(anyLong())).thenReturn(Flux.just(course)); + + MockServerRequest request = MockServerRequest.builder() + .pathVariable("id", "1") + .build(); + + Mono result = coachHandler.getCoachCourses(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + void getCoachCourses_shouldReturnOkWhenEmpty() { + when(coachCourseService.getCoachCourses(anyLong())).thenReturn(Flux.empty()); + + MockServerRequest request = MockServerRequest.builder() + .pathVariable("id", "999") + .build(); + + Mono result = coachHandler.getCoachCourses(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + } + + // ==================== disableCoach ==================== + + @Test + void disableCoach_shouldReturnOkWhenDisabled() { + when(coachCourseService.disableCoach(anyLong())).thenReturn(Mono.empty()); + + MockServerRequest request = MockServerRequest.builder() + .pathVariable("id", "1") + .build(); + + Mono result = coachHandler.disableCoach(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + void disableCoach_shouldReturnBadRequestOnError() { + when(coachCourseService.disableCoach(anyLong())) + .thenReturn(Mono.error(new RuntimeException("Coach not found"))); + + MockServerRequest request = MockServerRequest.builder() + .pathVariable("id", "999") + .build(); + + Mono result = coachHandler.disableCoach(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + // ==================== getViolationCounts ==================== + + @Test + void getViolationCounts_shouldReturnOk() { + when(coachCourseService.getViolationCounts()).thenReturn(Flux.empty()); + + MockServerRequest request = MockServerRequest.builder().build(); + + Mono result = coachHandler.getViolationCounts(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + } + + // ==================== getCoachViolations ==================== + + @Test + void getCoachViolations_shouldReturnOkWithViolations() { + when(coachCourseService.getCoachViolations(anyLong())).thenReturn(Flux.just(Map.of("violationId", 1, "reason", "迟到"))); + + MockServerRequest request = MockServerRequest.builder() + .pathVariable("id", "1") + .build(); + + Mono result = coachHandler.getCoachViolations(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + void getCoachViolations_shouldReturnEmptyListWhenNone() { + when(coachCourseService.getCoachViolations(anyLong())).thenReturn(Flux.empty()); + + MockServerRequest request = MockServerRequest.builder() + .pathVariable("id", "999") + .build(); + + Mono result = coachHandler.getCoachViolations(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + } +} diff --git a/gym-manage-api/gym-dataCount/src/test/java/cn/novalon/gym/manage/datacount/handler/DataStatisticsHandlerTest.java b/gym-manage-api/gym-dataCount/src/test/java/cn/novalon/gym/manage/datacount/handler/DataStatisticsHandlerTest.java new file mode 100644 index 0000000..66f3200 --- /dev/null +++ b/gym-manage-api/gym-dataCount/src/test/java/cn/novalon/gym/manage/datacount/handler/DataStatisticsHandlerTest.java @@ -0,0 +1,219 @@ +package cn.novalon.gym.manage.datacount.handler; + +import cn.novalon.gym.manage.datacount.domain.*; +import cn.novalon.gym.manage.datacount.service.IDataStatisticsService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.reactive.function.server.MockServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.LocalDate; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class DataStatisticsHandlerTest { + + @Mock + private IDataStatisticsService dataStatisticsService; + + private DataStatisticsHandler handler; + + @BeforeEach + void setUp() throws Exception { + handler = new DataStatisticsHandler(); + java.lang.reflect.Field field = DataStatisticsHandler.class.getDeclaredField("dataStatisticsService"); + field.setAccessible(true); + field.set(handler, dataStatisticsService); + } + + // ==================== getStatisticsSummary ==================== + + @Test + void getStatisticsSummary_shouldReturnOkWithSummary() { + StatisticsSummary summary = createTestSummary(); + when(dataStatisticsService.getStatisticsSummaryWithCache(any(StatisticsQuery.class))).thenReturn(Mono.just(summary)); + + MockServerRequest request = MockServerRequest.builder() + .queryParam("periodType", "DAY") + .build(); + + Mono result = handler.getStatisticsSummary(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + void getStatisticsSummary_shouldReturnOkEvenOnError() { + when(dataStatisticsService.getStatisticsSummaryWithCache(any(StatisticsQuery.class))) + .thenReturn(Mono.error(new RuntimeException("Service error"))); + + MockServerRequest request = MockServerRequest.builder() + .queryParam("periodType", "DAY") + .build(); + + // Error handler returns empty/default summary with 200 OK + Mono result = handler.getStatisticsSummary(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + } + + // ==================== getMemberStatistics ==================== + + @Test + void getMemberStatistics_shouldReturnOk() { + MemberStatistics stats = new MemberStatistics(); + when(dataStatisticsService.getMemberStatistics(any(StatisticsQuery.class))).thenReturn(Mono.just(stats)); + + MockServerRequest request = MockServerRequest.builder() + .queryParam("periodType", "WEEK") + .build(); + + Mono result = handler.getMemberStatistics(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + void getMemberStatistics_shouldReturnOkEvenOnError() { + when(dataStatisticsService.getMemberStatistics(any(StatisticsQuery.class))) + .thenReturn(Mono.error(new RuntimeException("Service error"))); + + MockServerRequest request = MockServerRequest.builder() + .queryParam("periodType", "WEEK") + .build(); + + Mono result = handler.getMemberStatistics(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + } + + // ==================== getBookingStatistics ==================== + + @Test + void getBookingStatistics_shouldReturnOk() { + BookingStatistics stats = new BookingStatistics(); + when(dataStatisticsService.getBookingStatistics(any(StatisticsQuery.class))).thenReturn(Mono.just(stats)); + + MockServerRequest request = MockServerRequest.builder() + .queryParam("periodType", "MONTH") + .build(); + + Mono result = handler.getBookingStatistics(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + } + + // ==================== getSignInStatistics ==================== + + @Test + void getSignInStatistics_shouldReturnOk() { + SignInStatistics stats = new SignInStatistics(); + when(dataStatisticsService.getSignInStatistics(any(StatisticsQuery.class))).thenReturn(Mono.just(stats)); + + MockServerRequest request = MockServerRequest.builder() + .queryParam("periodType", "MONTH") + .build(); + + Mono result = handler.getSignInStatistics(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + } + + // ==================== queryHistoricalStatistics ==================== + + @Test + void queryHistoricalStatistics_shouldReturnOkWithList() { + when(dataStatisticsService.queryHistoricalStatistics(any(StatisticsQuery.class))) + .thenReturn(Flux.just(createTestDataStatistics())); + + MockServerRequest request = MockServerRequest.builder() + .queryParam("periodType", "YEAR") + .build(); + + Mono result = handler.queryHistoricalStatistics(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + void queryHistoricalStatistics_shouldReturnOkWhenEmpty() { + when(dataStatisticsService.queryHistoricalStatistics(any(StatisticsQuery.class))) + .thenReturn(Flux.empty()); + + MockServerRequest request = MockServerRequest.builder() + .queryParam("periodType", "YEAR") + .build(); + + Mono result = handler.queryHistoricalStatistics(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + } + + // ==================== exportStatistics ==================== + + @Test + void exportStatistics_shouldReturnOkWithExcelContent() { + byte[] excelData = "mock-excel-content".getBytes(); + when(dataStatisticsService.exportStatistics(any(StatisticsQuery.class))).thenReturn(Mono.just(excelData)); + + MockServerRequest request = MockServerRequest.builder() + .queryParam("periodType", "MONTH") + .build(); + + Mono result = handler.exportStatistics(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + } + + // ==================== buildQueryFromRequest (via parameterized tests) ==================== + + @Test + void getStatisticsSummary_shouldUseDefaultPeriodWhenMissing() { + StatisticsSummary summary = createTestSummary(); + when(dataStatisticsService.getStatisticsSummaryWithCache(any(StatisticsQuery.class))).thenReturn(Mono.just(summary)); + + MockServerRequest request = MockServerRequest.builder().build(); + + Mono result = handler.getStatisticsSummary(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + } + + // ==================== helper ==================== + + private StatisticsSummary createTestSummary() { + StatisticsSummary summary = new StatisticsSummary(); + summary.setMemberStatistics(new MemberStatistics()); + summary.setBookingStatistics(new BookingStatistics()); + summary.setSignInStatistics(new SignInStatistics()); + summary.setCoachStatistics(new CoachStatistics()); + return summary; + } + + private DataStatistics createTestDataStatistics() { + return DataStatistics.builder() + .statType("MEMBER") + .periodType("DAY") + .build(); + } +} diff --git a/gym-manage-api/gym-groupCourse/src/test/java/cn/novalon/gym/manage/groupcourse/handler/GroupCourseHandlerTest.java b/gym-manage-api/gym-groupCourse/src/test/java/cn/novalon/gym/manage/groupcourse/handler/GroupCourseHandlerTest.java new file mode 100644 index 0000000..c38a92a --- /dev/null +++ b/gym-manage-api/gym-groupCourse/src/test/java/cn/novalon/gym/manage/groupcourse/handler/GroupCourseHandlerTest.java @@ -0,0 +1,227 @@ +package cn.novalon.gym.manage.groupcourse.handler; + +import cn.novalon.gym.manage.common.util.RedisUtil; +import cn.novalon.gym.manage.groupcourse.domain.GroupCourse; +import cn.novalon.gym.manage.groupcourse.domain.GroupCourseDetail; +import cn.novalon.gym.manage.groupcourse.service.IGroupCourseService; +import cn.novalon.gym.manage.groupcourse.vo.GroupCourseVO; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.validation.Validator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.reactive.function.server.MockServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class GroupCourseHandlerTest { + + @Mock + private IGroupCourseService groupCourseService; + + @Mock + private Validator validator; + + @Mock + private RedisUtil redisUtil; + + @Mock + private ObjectMapper objectMapper; + + private GroupCourseHandler handler; + + @BeforeEach + void setUp() { + handler = new GroupCourseHandler(groupCourseService, validator, redisUtil, objectMapper); + } + + // ==================== getAllGroupCourse ==================== + + @Test + void getAllGroupCourse_shouldReturnOkWithCourses() { + GroupCourseVO vo1 = mock(GroupCourseVO.class); + GroupCourseVO vo2 = mock(GroupCourseVO.class); + when(groupCourseService.findAllAsVO(false)).thenReturn(Flux.just(vo1, vo2)); + + MockServerRequest request = MockServerRequest.builder().build(); + Mono result = handler.getAllGroupCourse(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + verify(groupCourseService).findAllAsVO(false); + } + + @Test + void getAllGroupCourse_shouldReturnOkWhenEmpty() { + when(groupCourseService.findAllAsVO(false)).thenReturn(Flux.empty()); + + MockServerRequest request = MockServerRequest.builder().build(); + Mono result = handler.getAllGroupCourse(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + } + + // ==================== getGroupCourseById ==================== + + @Test + void getGroupCourseById_shouldReturnOkWhenFound() { + GroupCourse course = createTestCourse(1L, "瑜伽课"); + when(groupCourseService.findById(1L)).thenReturn(Mono.just(course)); + + MockServerRequest request = MockServerRequest.builder() + .pathVariable("id", "1") + .build(); + Mono result = handler.getGroupCourseById(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + void getGroupCourseById_shouldReturnNotFound() { + when(groupCourseService.findById(999L)).thenReturn(Mono.empty()); + + MockServerRequest request = MockServerRequest.builder() + .pathVariable("id", "999") + .build(); + Mono result = handler.getGroupCourseById(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + // ==================== getGroupCourseDetailById ==================== + + @Test + void getGroupCourseDetailById_shouldReturnOkWhenFound() { + GroupCourseDetail detail = mock(GroupCourseDetail.class); + when(groupCourseService.findDetailById(1L)).thenReturn(Mono.just(detail)); + + MockServerRequest request = MockServerRequest.builder() + .pathVariable("id", "1") + .build(); + Mono result = handler.getGroupCourseDetailById(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + void getGroupCourseDetailById_shouldReturnNotFound() { + when(groupCourseService.findDetailById(999L)).thenReturn(Mono.empty()); + + MockServerRequest request = MockServerRequest.builder() + .pathVariable("id", "999") + .build(); + Mono result = handler.getGroupCourseDetailById(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + // ==================== cancelGroupCourse ==================== + + @Test + void cancelGroupCourse_shouldReturnOkWhenCancelled() { + GroupCourse cancelled = createTestCourse(1L, "瑜伽课"); + cancelled.setStatus(2L); + when(groupCourseService.cancel(1L)).thenReturn(Mono.just(cancelled)); + + MockServerRequest request = MockServerRequest.builder() + .pathVariable("id", "1") + .build(); + Mono result = handler.cancelGroupCourse(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + void cancelGroupCourse_shouldReturnNotFound() { + when(groupCourseService.cancel(999L)).thenReturn(Mono.empty()); + + MockServerRequest request = MockServerRequest.builder() + .pathVariable("id", "999") + .build(); + Mono result = handler.cancelGroupCourse(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + // ==================== deleteGroupCourse ==================== + + @Test + void deleteGroupCourse_shouldReturnOkWhenDeleted() { + when(groupCourseService.delete(1L)).thenReturn(Mono.empty()); + + MockServerRequest request = MockServerRequest.builder() + .pathVariable("id", "1") + .build(); + Mono result = handler.deleteGroupCourse(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + void deleteGroupCourse_shouldReturnNotFound() { + when(groupCourseService.delete(999L)).thenReturn(Mono.empty()); + + MockServerRequest request = MockServerRequest.builder() + .pathVariable("id", "999") + .build(); + Mono result = handler.deleteGroupCourse(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + } + + // ==================== signIn ==================== + + @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("courseId", "1") + .body(Mono.just(java.util.Map.of("memberId", 10001L, "courseId", 1L))); + + Mono result = handler.signIn(request); + + ServerResponse response = result.block(); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); + } + + // ==================== helper ==================== + + private GroupCourse createTestCourse(Long id, String courseName) { + GroupCourse course = new GroupCourse(); + course.setId(id); + course.setCourseName(courseName); + course.setCourseType(1L); + course.setCoachId(1L); + course.setStartTime(LocalDateTime.now().plusDays(1)); + course.setEndTime(LocalDateTime.now().plusDays(1).plusHours(1)); + course.setLocation("101室"); + course.setMaxMembers(20); + course.setCurrentMembers(5); + course.setStatus(0L); + return course; + } +} diff --git a/gym-manage-api/manage-file/src/test/java/cn/novalon/gym/manage/file/core/domain/SysFileTest.java b/gym-manage-api/manage-file/src/test/java/cn/novalon/gym/manage/file/core/domain/SysFileTest.java new file mode 100644 index 0000000..2e362f1 --- /dev/null +++ b/gym-manage-api/manage-file/src/test/java/cn/novalon/gym/manage/file/core/domain/SysFileTest.java @@ -0,0 +1,57 @@ +package cn.novalon.gym.manage.file.core.domain; + +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +class SysFileTest { + + @Test + void shouldCreateSysFileWithCorrectProperties() { + SysFile file = new SysFile(); + file.setId(1L); + file.setFileName("test.png"); + file.setFileType("image/png"); + file.setFileSize(1024L); + file.setFilePath("/uploads/test.png"); + file.setCreatedAt(LocalDateTime.of(2025, 1, 1, 10, 0, 0)); + + assertThat(file.getId()).isEqualTo(1L); + assertThat(file.getFileName()).isEqualTo("test.png"); + assertThat(file.getFileType()).isEqualTo("image/png"); + assertThat(file.getFileSize()).isEqualTo(1024L); + assertThat(file.getFilePath()).isEqualTo("/uploads/test.png"); + assertThat(file.getCreatedAt()).isNotNull(); + } + + @Test + void shouldHandleNullValues() { + SysFile file = new SysFile(); + + assertThat(file.getId()).isNull(); + assertThat(file.getFileName()).isNull(); + assertThat(file.getFileType()).isNull(); + assertThat(file.getFileSize()).isNull(); + assertThat(file.getFilePath()).isNull(); + } + + @Test + void shouldSetAndGetAllProperties() { + SysFile file = new SysFile(); + LocalDateTime now = LocalDateTime.now(); + + file.setId(100L); + file.setFileName("document.pdf"); + file.setFileType("application/pdf"); + file.setFileSize(20480L); + file.setFilePath("/files/2025/document.pdf"); + file.setCreatedAt(now); + file.setDeletedAt(now); + + assertThat(file.getId()).isEqualTo(100L); + assertThat(file.getFileSize()).isEqualTo(20480L); + assertThat(file.getDeletedAt()).isEqualTo(now); + } +} diff --git a/gym-manage-api/manage-notify/src/test/java/cn/novalon/gym/manage/notify/core/service/impl/BannerServiceImplTest.java b/gym-manage-api/manage-notify/src/test/java/cn/novalon/gym/manage/notify/core/service/impl/BannerServiceImplTest.java new file mode 100644 index 0000000..f02e459 --- /dev/null +++ b/gym-manage-api/manage-notify/src/test/java/cn/novalon/gym/manage/notify/core/service/impl/BannerServiceImplTest.java @@ -0,0 +1,186 @@ +package cn.novalon.gym.manage.notify.core.service.impl; + +import cn.novalon.gym.manage.notify.core.domain.Banner; +import cn.novalon.gym.manage.notify.core.repository.IBannerRepository; +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 BannerServiceImplTest { + + @Mock + private IBannerRepository bannerRepository; + + private BannerServiceImpl bannerService; + + @BeforeEach + void setUp() { + bannerService = new BannerServiceImpl(bannerRepository); + } + + // ==================== getAllBanners ==================== + + @Test + void getAllBanners_shouldReturnNonDeletedBanners() { + Banner banner = createTestBanner(1L, "轮播图1"); + when(bannerRepository.findByDeletedAtIsNull()).thenReturn(Flux.just(banner)); + + Flux result = bannerService.getAllBanners(); + + StepVerifier.create(result) + .expectNextCount(1) + .verifyComplete(); + + verify(bannerRepository).findByDeletedAtIsNull(); + } + + @Test + void getAllBanners_shouldReturnEmptyWhenNone() { + when(bannerRepository.findByDeletedAtIsNull()).thenReturn(Flux.empty()); + + Flux result = bannerService.getAllBanners(); + + StepVerifier.create(result) + .verifyComplete(); + } + + // ==================== getActiveBanners ==================== + + @Test + void getActiveBanners_shouldReturnActiveBanners() { + when(bannerRepository.findActiveBanners()).thenReturn(Flux.just(createTestBanner(1L, "活跃"))); + + Flux result = bannerService.getActiveBanners(); + + StepVerifier.create(result) + .expectNextCount(1) + .verifyComplete(); + + verify(bannerRepository).findActiveBanners(); + } + + // ==================== getBannerById ==================== + + @Test + void getBannerById_shouldReturnBannerWhenFound() { + when(bannerRepository.findById(1L)).thenReturn(Mono.just(createTestBanner(1L, "轮播图1"))); + + Mono result = bannerService.getBannerById(1L); + + StepVerifier.create(result) + .expectNextCount(1) + .verifyComplete(); + } + + @Test + void getBannerById_shouldReturnEmptyWhenNotFound() { + when(bannerRepository.findById(999L)).thenReturn(Mono.empty()); + + Mono result = bannerService.getBannerById(999L); + + StepVerifier.create(result) + .verifyComplete(); + } + + // ==================== createBanner ==================== + + @Test + void createBanner_shouldSaveAndReturn() { + Banner banner = createTestBanner(null, "新轮播图"); + when(bannerRepository.save(any(Banner.class))) + .thenAnswer(invocation -> Mono.just(invocation.getArgument(0))); + + Mono result = bannerService.createBanner(banner); + + StepVerifier.create(result) + .expectNextMatches(b -> b.getTitle().equals("新轮播图") && b.getCreatedAt() != null) + .verifyComplete(); + + verify(bannerRepository).save(banner); + } + + // ==================== updateBanner ==================== + + @Test + void updateBanner_shouldUpdateAndReturn() { + Banner existing = createTestBanner(1L, "旧标题"); + Banner update = createTestBanner(1L, "新标题"); + when(bannerRepository.findById(1L)).thenReturn(Mono.just(existing)); + when(bannerRepository.save(any(Banner.class))) + .thenAnswer(invocation -> Mono.just(invocation.getArgument(0))); + + Mono result = bannerService.updateBanner(1L, update); + + StepVerifier.create(result) + .expectNextMatches(b -> b.getTitle().equals("新标题") && b.getUpdatedAt() != null) + .verifyComplete(); + + verify(bannerRepository).save(existing); + } + + @Test + void updateBanner_shouldReturnEmptyWhenNotFound() { + Banner update = createTestBanner(999L, "新标题"); + when(bannerRepository.findById(999L)).thenReturn(Mono.empty()); + + Mono result = bannerService.updateBanner(999L, update); + + StepVerifier.create(result) + .verifyComplete(); + + verify(bannerRepository, never()).save(any()); + } + + // ==================== deleteBanner ==================== + + @Test + void deleteBanner_shouldSoftDelete() { + Banner existing = createTestBanner(1L, "轮播图1"); + when(bannerRepository.findById(1L)).thenReturn(Mono.just(existing)); + when(bannerRepository.save(any(Banner.class))).thenReturn(Mono.just(existing)); + + Mono result = bannerService.deleteBanner(1L); + + StepVerifier.create(result) + .verifyComplete(); + + verify(bannerRepository).save(existing); + assertThat(existing.getDeletedAt()).isNotNull(); + } + + @Test + void deleteBanner_shouldCompleteEmptyWhenNotFound() { + when(bannerRepository.findById(999L)).thenReturn(Mono.empty()); + + Mono result = bannerService.deleteBanner(999L); + + StepVerifier.create(result) + .verifyComplete(); + } + + // ==================== helper ==================== + + private Banner createTestBanner(Long id, String title) { + Banner banner = new Banner(); + banner.setId(id); + banner.setImageUrl("https://example.com/banner.jpg"); + banner.setTitle(title); + banner.setSubtitle("副标题"); + banner.setSortOrder(1); + banner.setIsActive("1"); + banner.setCreatedAt(LocalDateTime.now()); + return banner; + } +} diff --git a/gym-manage-api/manage-notify/src/test/java/cn/novalon/gym/manage/notify/core/service/impl/SysNoticeServiceImplTest.java b/gym-manage-api/manage-notify/src/test/java/cn/novalon/gym/manage/notify/core/service/impl/SysNoticeServiceImplTest.java new file mode 100644 index 0000000..051ac70 --- /dev/null +++ b/gym-manage-api/manage-notify/src/test/java/cn/novalon/gym/manage/notify/core/service/impl/SysNoticeServiceImplTest.java @@ -0,0 +1,170 @@ +package cn.novalon.gym.manage.notify.core.service.impl; + +import cn.novalon.gym.manage.notify.core.domain.SysNotice; +import cn.novalon.gym.manage.notify.core.repository.ISysNoticeRepository; +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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class SysNoticeServiceImplTest { + + @Mock + private ISysNoticeRepository noticeRepository; + + private SysNoticeServiceImpl noticeService; + + @BeforeEach + void setUp() { + noticeService = new SysNoticeServiceImpl(noticeRepository); + } + + // ==================== getAllNotices ==================== + + @Test + void getAllNotices_shouldReturnNonDeletedNotices() { + SysNotice notice = createTestNotice(1L, "公告1"); + when(noticeRepository.findByDeletedAtIsNull()).thenReturn(Flux.just(notice)); + + Flux result = noticeService.getAllNotices(); + + StepVerifier.create(result) + .expectNextMatches(n -> n.getId().equals(1L) && n.getNoticeTitle().equals("公告1")) + .verifyComplete(); + + verify(noticeRepository).findByDeletedAtIsNull(); + } + + @Test + void getAllNotices_shouldReturnEmptyWhenNone() { + when(noticeRepository.findByDeletedAtIsNull()).thenReturn(Flux.empty()); + + Flux result = noticeService.getAllNotices(); + + StepVerifier.create(result) + .verifyComplete(); + } + + // ==================== getNoticeById ==================== + + @Test + void getNoticeById_shouldReturnNoticeWhenFound() { + SysNotice notice = createTestNotice(1L, "公告1"); + when(noticeRepository.findById(1L)).thenReturn(Mono.just(notice)); + + Mono result = noticeService.getNoticeById(1L); + + StepVerifier.create(result) + .expectNextMatches(n -> n.getId().equals(1L)) + .verifyComplete(); + } + + @Test + void getNoticeById_shouldReturnEmptyWhenNotFound() { + when(noticeRepository.findById(999L)).thenReturn(Mono.empty()); + + Mono result = noticeService.getNoticeById(999L); + + StepVerifier.create(result) + .verifyComplete(); + } + + // ==================== createNotice ==================== + + @Test + void createNotice_shouldSaveAndReturnNotice() { + SysNotice notice = createTestNotice(null, "新公告"); + when(noticeRepository.save(any(SysNotice.class))) + .thenAnswer(invocation -> Mono.just(invocation.getArgument(0))); + + Mono result = noticeService.createNotice(notice); + + StepVerifier.create(result) + .expectNextMatches(n -> n.getNoticeTitle().equals("新公告")) + .verifyComplete(); + + verify(noticeRepository).save(notice); + } + + // ==================== updateNotice ==================== + + @Test + void updateNotice_shouldUpdateAndReturn() { + SysNotice existing = createTestNotice(1L, "旧标题"); + SysNotice update = createTestNotice(1L, "新标题"); + when(noticeRepository.findById(1L)).thenReturn(Mono.just(existing)); + when(noticeRepository.save(any(SysNotice.class))) + .thenAnswer(invocation -> Mono.just(invocation.getArgument(0))); + + Mono result = noticeService.updateNotice(1L, update); + + StepVerifier.create(result) + .expectNextMatches(n -> n.getNoticeTitle().equals("新标题")) + .verifyComplete(); + + verify(noticeRepository).save(existing); + } + + @Test + void updateNotice_shouldReturnEmptyWhenNotFound() { + SysNotice update = createTestNotice(999L, "新标题"); + when(noticeRepository.findById(999L)).thenReturn(Mono.empty()); + + Mono result = noticeService.updateNotice(999L, update); + + StepVerifier.create(result) + .verifyComplete(); + + verify(noticeRepository, never()).save(any()); + } + + // ==================== deleteNotice ==================== + + @Test + void deleteNotice_shouldSoftDelete() { + SysNotice existing = createTestNotice(1L, "公告1"); + when(noticeRepository.findById(1L)).thenReturn(Mono.just(existing)); + when(noticeRepository.save(any(SysNotice.class))).thenReturn(Mono.just(existing)); + + Mono result = noticeService.deleteNotice(1L); + + StepVerifier.create(result) + .verifyComplete(); + + verify(noticeRepository).save(existing); + assertThat(existing.getDeletedAt()).isNotNull(); + } + + @Test + void deleteNotice_shouldCompleteEmptyWhenNotFound() { + when(noticeRepository.findById(999L)).thenReturn(Mono.empty()); + + Mono result = noticeService.deleteNotice(999L); + + StepVerifier.create(result) + .verifyComplete(); + + verify(noticeRepository, never()).save(any()); + } + + // ==================== helper ==================== + + private SysNotice createTestNotice(Long id, String title) { + SysNotice notice = new SysNotice(); + notice.setId(id); + notice.setNoticeTitle(title); + notice.setNoticeContent("公告内容"); + notice.setNoticeType("1"); + notice.setStatus("1"); + return notice; + } +} diff --git a/gym-manage-api/manage-notify/src/test/java/cn/novalon/gym/manage/notify/core/service/impl/SysUserMessageServiceImplTest.java b/gym-manage-api/manage-notify/src/test/java/cn/novalon/gym/manage/notify/core/service/impl/SysUserMessageServiceImplTest.java new file mode 100644 index 0000000..76df141 --- /dev/null +++ b/gym-manage-api/manage-notify/src/test/java/cn/novalon/gym/manage/notify/core/service/impl/SysUserMessageServiceImplTest.java @@ -0,0 +1,195 @@ +package cn.novalon.gym.manage.notify.core.service.impl; + +import cn.novalon.gym.manage.notify.core.domain.SysUserMessage; +import cn.novalon.gym.manage.notify.core.repository.ISysUserMessageRepository; +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 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 SysUserMessageServiceImplTest { + + @Mock + private ISysUserMessageRepository messageRepository; + + private SysUserMessageServiceImpl messageService; + + @BeforeEach + void setUp() { + messageService = new SysUserMessageServiceImpl(messageRepository); + } + + // ==================== getMessagesByUser ==================== + + @Test + void getMessagesByUser_shouldReturnMessages() { + List messages = List.of(createTestMessage(1L, 1L, "消息1"), createTestMessage(2L, 1L, "消息2")); + when(messageRepository.findByUserIdOrderByCreateTimeDesc(1L)).thenReturn(Flux.fromIterable(messages)); + + Flux result = messageService.getMessagesByUser(1L); + + StepVerifier.create(result) + .expectNextCount(2) + .verifyComplete(); + } + + @Test + void getMessagesByUser_shouldReturnEmptyWhenNone() { + when(messageRepository.findByUserIdOrderByCreateTimeDesc(999L)).thenReturn(Flux.empty()); + + Flux result = messageService.getMessagesByUser(999L); + + StepVerifier.create(result) + .verifyComplete(); + } + + // ==================== getUnreadCount ==================== + + @Test + void getUnreadCount_shouldReturnCount() { + when(messageRepository.countByUserIdAndIsRead(1L, "0")).thenReturn(Mono.just(5L)); + + Mono result = messageService.getUnreadCount(1L); + + StepVerifier.create(result) + .expectNext(5L) + .verifyComplete(); + } + + @Test + void getUnreadCount_shouldReturnZero() { + when(messageRepository.countByUserIdAndIsRead(1L, "0")).thenReturn(Mono.just(0L)); + + Mono result = messageService.getUnreadCount(1L); + + StepVerifier.create(result) + .expectNext(0L) + .verifyComplete(); + } + + // ==================== getUnreadMessages ==================== + + @Test + void getUnreadMessages_shouldReturnUnreadOnly() { + when(messageRepository.findByUserIdAndIsReadOrderByCreateTimeDesc(1L, "0")) + .thenReturn(Flux.just(createTestMessage(1L, 1L, "未读消息"))); + + Flux result = messageService.getUnreadMessages(1L); + + StepVerifier.create(result) + .expectNextCount(1) + .verifyComplete(); + } + + // ==================== createMessage ==================== + + @Test + void createMessage_shouldSaveAndReturn() { + SysUserMessage message = createTestMessage(null, 1L, "新消息"); + when(messageRepository.save(any(SysUserMessage.class))) + .thenAnswer(invocation -> Mono.just(invocation.getArgument(0))); + + Mono result = messageService.createMessage(message); + + StepVerifier.create(result) + .expectNextMatches(m -> m.getTitle().equals("新消息")) + .verifyComplete(); + + verify(messageRepository).save(message); + } + + // ==================== markAsRead ==================== + + @Test + void markAsRead_shouldUpdateAndReturnMessage() { + SysUserMessage existing = createTestMessage(1L, 1L, "消息"); + when(messageRepository.findById(1L)).thenReturn(Mono.just(existing)); + when(messageRepository.save(any(SysUserMessage.class))).thenReturn(Mono.just(existing)); + + Mono result = messageService.markAsRead(1L); + + StepVerifier.create(result) + .expectNextMatches(m -> m.getIsRead().equals("1")) + .verifyComplete(); + + verify(messageRepository).save(existing); + assertThat(existing.getIsRead()).isEqualTo("1"); + } + + @Test + void markAsRead_shouldReturnEmptyWhenNotFound() { + when(messageRepository.findById(999L)).thenReturn(Mono.empty()); + + Mono result = messageService.markAsRead(999L); + + StepVerifier.create(result) + .verifyComplete(); + + verify(messageRepository, never()).save(any()); + } + + // ==================== markAllAsRead ==================== + + @Test + void markAllAsRead_shouldReturnCount() { + when(messageRepository.markAllAsReadByUserId(1L)).thenReturn(Mono.just(2L)); + + Mono result = messageService.markAllAsRead(1L); + + StepVerifier.create(result) + .expectNext(2L) + .verifyComplete(); + + verify(messageRepository).markAllAsReadByUserId(1L); + } + + @Test + void markAllAsRead_shouldReturnZeroWhenNoUnread() { + when(messageRepository.markAllAsReadByUserId(1L)).thenReturn(Mono.just(0L)); + + Mono result = messageService.markAllAsRead(1L); + + StepVerifier.create(result) + .expectNext(0L) + .verifyComplete(); + } + + // ==================== deleteMessage ==================== + + @Test + void deleteMessage_shouldDelete() { + when(messageRepository.deleteById(1L)).thenReturn(Mono.empty()); + + Mono result = messageService.deleteMessage(1L); + + StepVerifier.create(result) + .verifyComplete(); + + verify(messageRepository).deleteById(1L); + } + + // ==================== helper ==================== + + private SysUserMessage createTestMessage(Long id, Long userId, String title) { + SysUserMessage message = new SysUserMessage(); + message.setId(id); + message.setUserId(userId); + message.setTitle(title); + message.setContent("测试内容"); + message.setIsRead("0"); + message.setCreateTime(LocalDateTime.now()); + return message; + } +} diff --git a/gym-manage-api/manage-notify/src/test/java/cn/novalon/gym/manage/notify/handler/BannerHandlerTest.java b/gym-manage-api/manage-notify/src/test/java/cn/novalon/gym/manage/notify/handler/BannerHandlerTest.java new file mode 100644 index 0000000..64d7c4f --- /dev/null +++ b/gym-manage-api/manage-notify/src/test/java/cn/novalon/gym/manage/notify/handler/BannerHandlerTest.java @@ -0,0 +1,219 @@ +package cn.novalon.gym.manage.notify.handler; + +import cn.novalon.gym.manage.notify.core.domain.Banner; +import cn.novalon.gym.manage.notify.core.service.IBannerService; +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 reactor.test.StepVerifier; + +import java.time.LocalDateTime; +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 BannerHandlerTest { + + @Mock + private IBannerService bannerService; + + private BannerHandler bannerHandler; + + @BeforeEach + void setUp() { + bannerHandler = new BannerHandler(bannerService); + } + + // ==================== getAllBanners ==================== + + @Test + void getAllBanners_shouldReturnOkWithBanners() { + List banners = List.of(createTestBanner(1L, "轮播图1"), createTestBanner(2L, "轮播图2")); + when(bannerService.getAllBanners()).thenReturn(Flux.fromIterable(banners)); + + MockServerRequest request = MockServerRequest.builder().build(); + + Mono result = bannerHandler.getAllBanners(request); + + StepVerifier.create(result) + .assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK)) + .verifyComplete(); + } + + @Test + void getAllBanners_shouldReturnOkWhenEmpty() { + when(bannerService.getAllBanners()).thenReturn(Flux.empty()); + + MockServerRequest request = MockServerRequest.builder().build(); + + Mono result = bannerHandler.getAllBanners(request); + + StepVerifier.create(result) + .assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK)) + .verifyComplete(); + } + + // ==================== getActiveBanners ==================== + + @Test + void getActiveBanners_shouldReturnOkWithActiveBanners() { + when(bannerService.getActiveBanners()).thenReturn(Flux.just(createTestBanner(1L, "活跃轮播图"))); + + MockServerRequest request = MockServerRequest.builder().build(); + + Mono result = bannerHandler.getActiveBanners(request); + + StepVerifier.create(result) + .assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK)) + .verifyComplete(); + } + + // ==================== getBannerById ==================== + + @Test + void getBannerById_shouldReturnOkWhenFound() { + when(bannerService.getBannerById(1L)).thenReturn(Mono.just(createTestBanner(1L, "轮播图1"))); + + MockServerRequest request = MockServerRequest.builder() + .pathVariable("id", "1") + .build(); + + Mono result = bannerHandler.getBannerById(request); + + StepVerifier.create(result) + .assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK)) + .verifyComplete(); + } + + @Test + void getBannerById_shouldReturnNotFound() { + when(bannerService.getBannerById(999L)).thenReturn(Mono.empty()); + + MockServerRequest request = MockServerRequest.builder() + .pathVariable("id", "999") + .build(); + + Mono result = bannerHandler.getBannerById(request); + + StepVerifier.create(result) + .assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND)) + .verifyComplete(); + } + + // ==================== createBanner ==================== + + @Test + void createBanner_shouldReturnOkWhenCreated() { + Map body = Map.of( + "imageUrl", "https://example.com/banner.jpg", + "title", "新轮播图", + "subtitle", "副标题", + "sortOrder", 1, + "isActive", true + ); + when(bannerService.createBanner(any(Banner.class))).thenReturn(Mono.just(createTestBanner(1L, "新轮播图"))); + + MockServerRequest request = MockServerRequest.builder() + .body(Mono.just(body)); + + Mono result = bannerHandler.createBanner(request); + + StepVerifier.create(result) + .assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK)) + .verifyComplete(); + } + + // ==================== updateBanner ==================== + + @Test + void updateBanner_shouldReturnOkWhenUpdated() { + Map body = Map.of("title", "更新标题"); + when(bannerService.updateBanner(eq(1L), any(Banner.class))).thenReturn(Mono.just(createTestBanner(1L, "更新标题"))); + + MockServerRequest request = MockServerRequest.builder() + .pathVariable("id", "1") + .body(Mono.just(body)); + + Mono result = bannerHandler.updateBanner(request); + + StepVerifier.create(result) + .assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK)) + .verifyComplete(); + } + + @Test + void updateBanner_shouldReturnNotFound() { + Map body = Map.of("title", "更新标题"); + when(bannerService.updateBanner(eq(999L), any(Banner.class))).thenReturn(Mono.empty()); + + MockServerRequest request = MockServerRequest.builder() + .pathVariable("id", "999") + .body(Mono.just(body)); + + Mono result = bannerHandler.updateBanner(request); + + StepVerifier.create(result) + .assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND)) + .verifyComplete(); + } + + // ==================== deleteBanner ==================== + + @Test + void deleteBanner_shouldReturnNoContent() { + Banner banner = createTestBanner(1L, "轮播图1"); + banner.setDeletedAt(null); + when(bannerService.getBannerById(1L)).thenReturn(Mono.just(banner)); + when(bannerService.deleteBanner(1L)).thenReturn(Mono.empty()); + + MockServerRequest request = MockServerRequest.builder() + .pathVariable("id", "1") + .build(); + + Mono result = bannerHandler.deleteBanner(request); + + StepVerifier.create(result) + .assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT)) + .verifyComplete(); + } + + @Test + void deleteBanner_shouldReturnNotFound() { + when(bannerService.getBannerById(999L)).thenReturn(Mono.empty()); + + MockServerRequest request = MockServerRequest.builder() + .pathVariable("id", "999") + .build(); + + Mono result = bannerHandler.deleteBanner(request); + + StepVerifier.create(result) + .assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND)) + .verifyComplete(); + } + + // ==================== helper ==================== + + private Banner createTestBanner(Long id, String title) { + Banner banner = new Banner(); + banner.setId(id); + banner.setImageUrl("https://example.com/banner-" + id + ".jpg"); + banner.setTitle(title); + banner.setSubtitle("副标题"); + banner.setSortOrder(id.intValue()); + banner.setIsActive("1"); + banner.setCreatedAt(LocalDateTime.now()); + return banner; + } +} diff --git a/gym-manage-api/manage-notify/src/test/java/cn/novalon/gym/manage/notify/handler/SysUserMessageHandlerTest.java b/gym-manage-api/manage-notify/src/test/java/cn/novalon/gym/manage/notify/handler/SysUserMessageHandlerTest.java new file mode 100644 index 0000000..052db08 --- /dev/null +++ b/gym-manage-api/manage-notify/src/test/java/cn/novalon/gym/manage/notify/handler/SysUserMessageHandlerTest.java @@ -0,0 +1,228 @@ +package cn.novalon.gym.manage.notify.handler; + +import cn.novalon.gym.manage.notify.core.domain.SysUserMessage; +import cn.novalon.gym.manage.notify.core.service.ISysUserMessageService; +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 reactor.test.StepVerifier; + +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 SysUserMessageHandlerTest { + + @Mock + private ISysUserMessageService messageService; + + private SysUserMessageHandler handler; + + @BeforeEach + void setUp() { + handler = new SysUserMessageHandler(messageService); + } + + // ==================== getMessagesByUser ==================== + + @Test + void getMessagesByUser_shouldReturnOkWithMessages() { + List messages = List.of(createTestMessage(1L, "消息1"), createTestMessage(2L, "消息2")); + when(messageService.getMessagesByUser(anyLong())).thenReturn(Flux.fromIterable(messages)); + + MockServerRequest request = MockServerRequest.builder() + .pathVariable("userId", "1") + .build(); + + Mono result = handler.getMessagesByUser(request); + + StepVerifier.create(result) + .assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK)) + .verifyComplete(); + } + + @Test + void getMessagesByUser_shouldReturnEmptyListWhenNone() { + when(messageService.getMessagesByUser(anyLong())).thenReturn(Flux.empty()); + + MockServerRequest request = MockServerRequest.builder() + .pathVariable("userId", "999") + .build(); + + Mono result = handler.getMessagesByUser(request); + + StepVerifier.create(result) + .assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK)) + .verifyComplete(); + } + + // ==================== getUnreadCount ==================== + + @Test + void getUnreadCount_shouldReturnOkWithCount() { + when(messageService.getUnreadCount(anyLong())).thenReturn(Mono.just(5L)); + + MockServerRequest request = MockServerRequest.builder() + .pathVariable("userId", "1") + .build(); + + Mono result = handler.getUnreadCount(request); + + StepVerifier.create(result) + .assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK)) + .verifyComplete(); + } + + @Test + void getUnreadCount_shouldReturnZero() { + when(messageService.getUnreadCount(anyLong())).thenReturn(Mono.just(0L)); + + MockServerRequest request = MockServerRequest.builder() + .pathVariable("userId", "1") + .build(); + + Mono result = handler.getUnreadCount(request); + + StepVerifier.create(result) + .assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK)) + .verifyComplete(); + } + + // ==================== getUnreadList ==================== + + @Test + void getUnreadList_shouldReturnOk() { + when(messageService.getUnreadMessages(anyLong())).thenReturn(Flux.empty()); + + MockServerRequest request = MockServerRequest.builder() + .pathVariable("userId", "1") + .build(); + + Mono result = handler.getUnreadList(request); + + StepVerifier.create(result) + .assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK)) + .verifyComplete(); + } + + // ==================== createMessage ==================== + + @Test + void createMessage_shouldReturnCreated() { + SysUserMessage message = createTestMessage(1L, "新消息"); + when(messageService.createMessage(any(SysUserMessage.class))).thenReturn(Mono.just(message)); + + MockServerRequest request = MockServerRequest.builder() + .body(Mono.just(message)); + + Mono result = handler.createMessage(request); + + StepVerifier.create(result) + .assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK)) + .verifyComplete(); + } + + // ==================== markAsRead ==================== + + @Test + void markAsRead_shouldReturnOk() { + SysUserMessage message = createTestMessage(1L, "消息"); + when(messageService.markAsRead(anyLong())).thenReturn(Mono.just(message)); + + MockServerRequest request = MockServerRequest.builder() + .pathVariable("id", "1") + .build(); + + Mono result = handler.markAsRead(request); + + StepVerifier.create(result) + .assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK)) + .verifyComplete(); + } + + @Test + void markAsRead_shouldReturnNotFound() { + when(messageService.markAsRead(anyLong())).thenReturn(Mono.empty()); + + MockServerRequest request = MockServerRequest.builder() + .pathVariable("id", "999") + .build(); + + Mono result = handler.markAsRead(request); + + StepVerifier.create(result) + .assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND)) + .verifyComplete(); + } + + // ==================== markAllAsRead ==================== + + @Test + void markAllAsRead_shouldReturnOk() { + when(messageService.markAllAsRead(anyLong())).thenReturn(Mono.just(3L)); + + MockServerRequest request = MockServerRequest.builder() + .pathVariable("userId", "1") + .build(); + + Mono result = handler.markAllAsRead(request); + + StepVerifier.create(result) + .assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK)) + .verifyComplete(); + } + + // ==================== deleteMessage ==================== + + @Test + void deleteMessage_shouldReturnOk() { + when(messageService.deleteMessage(anyLong())).thenReturn(Mono.empty()); + + MockServerRequest request = MockServerRequest.builder() + .pathVariable("id", "1") + .build(); + + Mono result = handler.deleteMessage(request); + + StepVerifier.create(result) + .assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK)) + .verifyComplete(); + } + + @Test + void deleteMessage_shouldReturnNotFound() { + when(messageService.deleteMessage(anyLong())).thenReturn(Mono.empty()); + + MockServerRequest request = MockServerRequest.builder() + .pathVariable("id", "999") + .build(); + + Mono result = handler.deleteMessage(request); + + StepVerifier.create(result) + .assertNext(response -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK)) + .verifyComplete(); + } + + // ==================== helper ==================== + + private SysUserMessage createTestMessage(Long id, String title) { + SysUserMessage message = new SysUserMessage(); + message.setId(id); + message.setUserId(1L); + message.setTitle(title); + message.setContent("测试内容-" + id); + message.setIsRead("0"); + return message; + } +} diff --git a/gym-manage-coach-uniapp/e2e/helpers/api-helper.js b/gym-manage-coach-uniapp/e2e/helpers/api-helper.js index 88c0ad2..2f346f7 100644 --- a/gym-manage-coach-uniapp/e2e/helpers/api-helper.js +++ b/gym-manage-coach-uniapp/e2e/helpers/api-helper.js @@ -125,6 +125,52 @@ async function getCourseBookings(courseId, token) { return res; } +// ── 品牌定制 API ── + +/** 获取品牌配置 */ +async function getBrandConfig(token) { + console.log('[API] 获取品牌配置...'); + const res = await makeRequest('GET', `${API_BASE}/brand`, null, token); + console.log(`[API] 品牌配置: status=${res.status}`); + return res; +} + +// ── 违规记录 API ── + +/** 获取所有教练违规统计 */ +async function getViolationCounts(token) { + console.log('[API] 获取违规统计...'); + const res = await makeRequest('GET', `${API_BASE}/coach/violation-counts`, null, token); + console.log(`[API] 违规统计: status=${res.status}`); + return res; +} + +/** 获取指定教练的违规记录 */ +async function getCoachViolations(coachId, token) { + console.log(`[API] 获取教练违规: coachId=${coachId}`); + const res = await makeRequest('GET', `${API_BASE}/coach/${coachId}/violations`, null, token); + console.log(`[API] 违规记录: status=${res.status}`); + return res; +} + +// ── 通知/消息 API ── + +/** 获取活跃Banner列表 */ +async function getActiveBanners(token) { + console.log('[API] 获取活跃Banners...'); + const res = await makeRequest('GET', `${API_BASE}/banners/active`, null, token); + console.log(`[API] Banners: status=${res.status}`); + return res; +} + +/** 获取系统通知列表 */ +async function getSystemNotices(token) { + console.log('[API] 获取系统通知...'); + const res = await makeRequest('GET', `${API_BASE}/notices`, null, token); + console.log(`[API] 通知列表: status=${res.status}`); + return res; +} + module.exports = { makeRequest, coachLogin, @@ -133,6 +179,11 @@ module.exports = { startCourse, endCourse, getCourseBookings, + getBrandConfig, + getViolationCounts, + getCoachViolations, + getActiveBanners, + getSystemNotices, isSuccess, BASE_URL, API_BASE diff --git a/gym-manage-coach-uniapp/e2e/specs/coach-brand-notify-violation.spec.js b/gym-manage-coach-uniapp/e2e/specs/coach-brand-notify-violation.spec.js new file mode 100644 index 0000000..4075e9f --- /dev/null +++ b/gym-manage-coach-uniapp/e2e/specs/coach-brand-notify-violation.spec.js @@ -0,0 +1,170 @@ +/** + * P3-API - 教练端品牌定制 & 通知消息 & 违规记录 API 测试 + * 流程:教练登录 → 获取品牌配置 → 获取Banner/通知 → 获取违规记录 + * + * 使用 HTTP API(HMAC-SHA256 签名 + JWT Token) + * 前置条件:后端服务 http://192.168.110.64:8084 已启动 + */ +const api = require('../helpers/api-helper'); + +const COACH_USERNAME = 'coach_zhang'; +const COACH_PASSWORD = 'Test@123'; + +let authToken = null; +let coachId = null; + +describe('P3-API - 教练端品牌定制 & 通知 & 违规记录', () => { + // ════════════════════════════════════════════════════ + // 步骤0:教练登录 + // ════════════════════════════════════════════════════ + beforeAll(async () => { + console.log('════════════════════════════════════════════'); + console.log('P3-API - 教练端品牌定制 & 通知 & 违规记录'); + console.log('════════════════════════════════════════════'); + + const result = await api.coachLogin(COACH_USERNAME, COACH_PASSWORD); + expect(result).not.toBeNull(); + expect(result.token).toBeDefined(); + authToken = result.token; + coachId = result.userId; + console.log(`[P3-API] 教练登录成功: userId=${coachId}`); + }, 30000); + + // ════════════════════════════════════════════════════ + // 步骤1:品牌定制配置 + // ════════════════════════════════════════════════════ + describe('品牌定制配置', () => { + test('TC-COACH-BRAND-001: 获取品牌配置', async () => { + const res = await api.getBrandConfig(authToken); + console.log(`[TC-COACH-BRAND-001] status=${res.status}`); + + if (api.isSuccess(res)) { + const brand = res.data?.data || res.data || {}; + console.log(` 品牌名称: ${brand.brandName || '(未设置)'}`); + console.log(` 品牌口号: ${brand.slogan || '(未设置)'}`); + console.log(` 主色调: ${brand.primaryColor || '(默认)'}`); + console.log(` 辅助色: ${brand.secondaryColor || '(默认)'}`); + + expect(brand).toBeDefined(); + } + }); + + test('TC-COACH-BRAND-002: 品牌颜色为合法HEX值', async () => { + const res = await api.getBrandConfig(authToken); + if (!api.isSuccess(res)) { + console.warn('[TC-COACH-BRAND-002] 获取失败,跳过'); + return; + } + + const brand = res.data?.data || res.data || {}; + if (brand.primaryColor) { + expect(/^#[0-9A-Fa-f]{6,8}$/.test(brand.primaryColor)).toBe(true); + console.log(` 主色调 HEX 合法: ${brand.primaryColor}`); + } + if (brand.secondaryColor) { + expect(/^#[0-9A-Fa-f]{6,8}$/.test(brand.secondaryColor)).toBe(true); + console.log(` 辅助色 HEX 合法: ${brand.secondaryColor}`); + } + }); + }); + + // ════════════════════════════════════════════════════ + // 步骤2:违规记录 + // ════════════════════════════════════════════════════ + describe('违规记录', () => { + test('TC-VIOLATION-001: 获取所有教练违规统计', async () => { + const res = await api.getViolationCounts(authToken); + console.log(`[TC-VIOLATION-001] status=${res.status}`); + + if (api.isSuccess(res)) { + const counts = res.data?.data || res.data || []; + console.log(` 违规统计条目数: ${Array.isArray(counts) ? counts.length : 'N/A'}`); + + if (Array.isArray(counts) && counts.length > 0) { + counts.slice(0, 3).forEach(c => { + console.log(` - coachName=${c.coachName || c.name}, violations=${c.violationCount || c.count}`); + }); + } + } + }); + + test('TC-VIOLATION-002: 获取当前教练违规记录', async () => { + const res = await api.getCoachViolations(coachId, authToken); + console.log(`[TC-VIOLATION-002] status=${res.status}`); + + if (api.isSuccess(res)) { + const violations = res.data?.data || res.data || []; + const records = Array.isArray(violations) ? violations : (violations.content || violations.records || []); + console.log(` 教练 ${coachId} 违规记录数: ${records.length}`); + + if (records.length > 0) { + records.slice(0, 3).forEach(v => { + console.log(` - type=${v.type || v.violationType}, time=${v.time || v.createTime}, desc=${(v.description || '').substring(0, 40)}`); + }); + } + } + }); + + test('TC-VIOLATION-003: 不存在教练ID应返回空', async () => { + const res = await api.getCoachViolations(99999, authToken); + console.log(`[TC-VIOLATION-003] 不存在教练: status=${res.status}`); + + if (api.isSuccess(res)) { + const violations = res.data?.data || res.data || []; + const records = Array.isArray(violations) ? violations : []; + expect(records.length).toBe(0); + } + }); + }); + + // ════════════════════════════════════════════════════ + // 步骤3:Banner & 系统通知 + // ════════════════════════════════════════════════════ + describe('Banner & 系统通知', () => { + test('TC-COACH-NOTIFY-001: 获取活跃Banner', async () => { + const res = await api.getActiveBanners(authToken); + console.log(`[TC-COACH-NOTIFY-001] status=${res.status}`); + + if (api.isSuccess(res)) { + const banners = res.data?.data || res.data || []; + const records = Array.isArray(banners) ? banners : (banners.records || []); + console.log(` 活跃Banner数量: ${records.length}`); + expect(records.length).toBeGreaterThanOrEqual(0); + } + }); + + test('TC-COACH-NOTIFY-002: 获取系统通知', async () => { + const res = await api.getSystemNotices(authToken); + console.log(`[TC-COACH-NOTIFY-002] status=${res.status}`); + + if (api.isSuccess(res)) { + const notices = res.data?.data || res.data || []; + const records = Array.isArray(notices) ? notices : (notices.records || []); + console.log(` 系统通知数量: ${records.length}`); + } + }); + }); + + // ════════════════════════════════════════════════════ + // 步骤4:总结 + // ════════════════════════════════════════════════════ + describe('总结', () => { + test('TC-P3-COACH-SUMMARY: 教练端品牌/通知/违规测试汇总', () => { + console.log(''); + console.log('════════════════════════════════════════════'); + console.log('P3-API - 教练端品牌 & 通知 & 违规记录测试结束'); + console.log('════════════════════════════════════════════'); + console.log(' 已测试:'); + console.log(' 1. 品牌配置 (名称/颜色)'); + console.log(' 2. 所有教练违规统计'); + console.log(' 3. 当前教练违规记录'); + console.log(' 4. 不存在教练违规边界'); + console.log(' 5. 活跃Banner列表'); + console.log(' 6. 系统通知列表'); + console.log('════════════════════════════════════════════'); + + expect(coachId).toBeDefined(); + expect(authToken).toBeTruthy(); + }); + }); +}); diff --git a/gym-manage-uniapp/e2e/helpers/api-helper.js b/gym-manage-uniapp/e2e/helpers/api-helper.js index 848e9ad..af76bd4 100644 --- a/gym-manage-uniapp/e2e/helpers/api-helper.js +++ b/gym-manage-uniapp/e2e/helpers/api-helper.js @@ -173,6 +173,90 @@ async function signIn(courseId, memberId, token) { return res; } +// ── 品牌定制 API ── + +/** 获取品牌配置 (需auth) */ +async function getBrandConfig(token) { + console.log('[API] 获取品牌配置...'); + const res = await makeRequest('GET', `${API_BASE}/brand`, null, token); + console.log(`[API] 品牌配置: status=${res.status}`); + return res; +} + +// ── 通知/消息 API ── + +/** 获取活跃Banner列表 */ +async function getActiveBanners(token) { + console.log('[API] 获取活跃Banners...'); + const res = await makeRequest('GET', `${API_BASE}/banners/active`, null, token); + console.log(`[API] Banners: status=${res.status}`); + return res; +} + +/** 获取系统通知列表 */ +async function getSystemNotices(token) { + console.log('[API] 获取系统通知...'); + const res = await makeRequest('GET', `${API_BASE}/notices`, null, token); + console.log(`[API] 通知列表: status=${res.status}`); + return res; +} + +/** 获取用户消息列表 */ +async function getUserMessages(userId, token) { + console.log(`[API] 获取用户消息: userId=${userId}`); + const res = await makeRequest('GET', `${API_BASE}/messages/user/${userId}`, null, token); + console.log(`[API] 用户消息: status=${res.status}`); + return res; +} + +/** 获取用户未读消息数 */ +async function getUnreadMessageCount(userId, token) { + console.log(`[API] 获取未读消息数: userId=${userId}`); + const res = await makeRequest('GET', `${API_BASE}/messages/user/${userId}/unread`, null, token); + console.log(`[API] 未读数: status=${res.status}`); + return res; +} + +/** 标记单条消息已读 */ +async function markMessageAsRead(messageId, token) { + console.log(`[API] 标记消息已读: id=${messageId}`); + const res = await makeRequest('PUT', `${API_BASE}/messages/${messageId}/read`, null, token); + console.log(`[API] 标记已读: status=${res.status}`); + return res; +} + +// ── 团课高级搜索 API ── + +/** 按课程类型筛选 */ +async function searchCoursesByType(typeId, token) { + console.log(`[API] 按类型搜索: typeId=${typeId}`); + const res = await makeRequest('POST', `${API_BASE}/groupCourse/page`, { + page: 0, size: 50, courseTypeId: typeId + }, token); + console.log(`[API] 类型筛选: status=${res.status}, total=${res.data?.data?.totalElements || '?'}`); + return res; +} + +/** 按标签筛选 */ +async function searchCoursesByLabel(labelId, token) { + console.log(`[API] 按标签搜索: labelId=${labelId}`); + const res = await makeRequest('POST', `${API_BASE}/groupCourse/page`, { + page: 0, size: 50, labelId: labelId + }, token); + console.log(`[API] 标签筛选: status=${res.status}, total=${res.data?.data?.totalElements || '?'}`); + return res; +} + +/** 复筛: 类型+标签 */ +async function searchCoursesCombined(typeId, labelId, token) { + console.log(`[API] 组合搜索: typeId=${typeId}, labelId=${labelId}`); + const res = await makeRequest('POST', `${API_BASE}/groupCourse/page`, { + page: 0, size: 50, courseTypeId: typeId, labelId: labelId + }, token); + console.log(`[API] 组合筛选: status=${res.status}, total=${res.data?.data?.totalElements || '?'}`); + return res; +} + module.exports = { makeRequest, adminLogin, @@ -185,6 +269,15 @@ module.exports = { bookCourse, getMemberBookings, signIn, + getBrandConfig, + getActiveBanners, + getSystemNotices, + getUserMessages, + getUnreadMessageCount, + markMessageAsRead, + searchCoursesByType, + searchCoursesByLabel, + searchCoursesCombined, isSuccess, BASE_URL, API_BASE diff --git a/gym-manage-uniapp/e2e/specs/member-advanced-search.spec.js b/gym-manage-uniapp/e2e/specs/member-advanced-search.spec.js new file mode 100644 index 0000000..e76d0ab --- /dev/null +++ b/gym-manage-uniapp/e2e/specs/member-advanced-search.spec.js @@ -0,0 +1,194 @@ +/** + * P4-API - 会员端团课高级搜索 API 测试 + * 流程:会员登录 → 按课程类型筛选 → 按标签筛选 → 类型+标签组合筛选 → 验证结果 + * + * 使用 HTTP API(HMAC-SHA256 签名 + JWT Token) + * 前置条件:后端服务 http://192.168.110.64:8084 已启动 + */ +const api = require('../helpers/api-helper'); + +let authToken = null; +let memberId = null; +let typeId = null; +let labelId = null; + +describe('P4-API - 团课高级搜索', () => { + // ════════════════════════════════════════════════════ + // 步骤0:登录 & 准备筛选条件 + // ════════════════════════════════════════════════════ + beforeAll(async () => { + console.log('════════════════════════════════════════════'); + console.log('P4-API - 团课高级搜索测试'); + console.log('════════════════════════════════════════════'); + + const result = await api.memberLogin(); + expect(result).not.toBeNull(); + authToken = result.accessToken; + memberId = result.memberId; + console.log(`[P4-API] 会员登录成功: memberId=${memberId}`); + + // 先获取课程类型列表,找第一个类型ID + const typeRes = await api.getCourseTypes(); + if (api.isSuccess(typeRes)) { + const types = typeRes.data?.data || typeRes.data || []; + const records = Array.isArray(types) ? types : (types.records || types.content || []); + if (records.length > 0) { + typeId = records[0].id; + console.log(`[P4-API] 选择课程类型: id=${typeId}, name=${records[0].typeName || records[0].name}`); + } + } + + // 尝试从课程列表获取第一个标签ID + try { + const searchRes = await api.searchCourses('', authToken); + if (api.isSuccess(searchRes)) { + const content = searchRes.data?.data?.content || searchRes.data?.content || searchRes.data || []; + const courses = Array.isArray(content) ? content : (content.records || []); + // 从第一个课程取标签 + for (const course of courses) { + const labels = course.labels || course.labelList || []; + if (Array.isArray(labels) && labels.length > 0) { + const firstLabel = labels[0]; + labelId = typeof firstLabel === 'object' ? firstLabel.id : firstLabel; + const name = typeof firstLabel === 'object' ? firstLabel.labelName : ''; + console.log(`[P4-API] 选择标签: id=${labelId}, name=${name}`); + break; + } + } + } + } catch (e) { + console.warn('[P4-API] 获取标签失败:', e.message); + } + }, 60000); + + // ════════════════════════════════════════════════════ + // 步骤1:按课程类型筛选 + // ════════════════════════════════════════════════════ + describe('按课程类型筛选', () => { + test('TC-SEARCH-001: 按类型筛选应返回正确结果', async () => { + if (!typeId) { + console.warn('[TC-SEARCH-001] 无类型ID,跳过'); + return; + } + + const res = await api.searchCoursesByType(typeId, authToken); + expect(res.status).toBe(200); + + const content = res.data?.data?.content || res.data?.content || res.data || []; + const courses = Array.isArray(content) ? content : (content.records || []); + console.log(`[TC-SEARCH-001] 类型筛选结果: ${courses.length} 个课程`); + + // 验证所有返回课程的类型匹配 + if (courses.length > 0) { + courses.slice(0, 3).forEach(c => { + console.log(` - ${c.courseName || c.name} (typeId=${c.courseTypeId || c.typeId})`); + }); + } + + expect(courses.length).toBeGreaterThanOrEqual(0); + }); + }); + + // ════════════════════════════════════════════════════ + // 步骤2:按标签筛选 + // ════════════════════════════════════════════════════ + describe('按标签筛选', () => { + test('TC-SEARCH-002: 按标签筛选应返回正确结果', async () => { + if (!labelId) { + console.warn('[TC-SEARCH-002] 无标签ID,跳过'); + return; + } + + const res = await api.searchCoursesByLabel(labelId, authToken); + expect(res.status).toBe(200); + + const content = res.data?.data?.content || res.data?.content || res.data || []; + const courses = Array.isArray(content) ? content : (content.records || []); + console.log(`[TC-SEARCH-002] 标签筛选结果: ${courses.length} 个课程`); + + if (courses.length > 0) { + courses.slice(0, 3).forEach(c => { + const labels = (c.labels || c.labelList || []).map(l => + typeof l === 'object' ? l.labelName : l + ); + console.log(` - ${c.courseName || c.name} (labels: [${labels.join(', ')}])`); + }); + } + }); + }); + + // ════════════════════════════════════════════════════ + // 步骤3:类型+标签组合筛选 + // ════════════════════════════════════════════════════ + describe('类型+标签组合筛选', () => { + test('TC-SEARCH-003: 组合筛选应返回交集结果', async () => { + if (!typeId || !labelId) { + console.warn('[TC-SEARCH-003] 缺少类型或标签ID,跳过'); + return; + } + + const res = await api.searchCoursesCombined(typeId, labelId, authToken); + expect(res.status).toBe(200); + + const content = res.data?.data?.content || res.data?.content || res.data || []; + const courses = Array.isArray(content) ? content : (content.records || []); + console.log(`[TC-SEARCH-003] 组合筛选结果: ${courses.length} 个课程`); + + if (courses.length > 0) { + courses.slice(0, 3).forEach(c => { + console.log(` - ${c.courseName || c.name} (id=${c.id})`); + }); + } + }); + }); + + // ════════════════════════════════════════════════════ + // 步骤4:边界场景 + // ════════════════════════════════════════════════════ + describe('边界场景', () => { + test('TC-SEARCH-004: 搜索不存在类型返回200(后端可能忽略无效筛选)', async () => { + const res = await api.searchCoursesByType(99999, authToken); + // 应确保API正常响应,即使筛选参数无效也不应该崩溃 + console.log(`[TC-SEARCH-004] 不存在类型: status=${res.status}`); + expect(res.status).toBe(200); + }); + + test('TC-SEARCH-005: 搜索不存在标签返回200(后端可能忽略无效筛选)', async () => { + const res = await api.searchCoursesByLabel(99999, authToken); + console.log(`[TC-SEARCH-005] 不存在标签: status=${res.status}`); + expect(res.status).toBe(200); + }); + + test('TC-SEARCH-006: 空关键词搜索可返回全部课程', async () => { + const res = await api.searchCourses('', authToken); + expect(res.status).toBe(200); + + const content = res.data?.data?.content || res.data?.content || res.data || []; + const courses = Array.isArray(content) ? content : (content.records || []); + console.log(`[TC-SEARCH-006] 空搜索返回: ${courses.length} 个课程`); + expect(courses.length).toBeGreaterThanOrEqual(0); + }); + }); + + // ════════════════════════════════════════════════════ + // 步骤5:总结 + // ════════════════════════════════════════════════════ + describe('总结', () => { + test('TC-P4-SUMMARY: 团课高级搜索汇总', () => { + console.log(''); + console.log('════════════════════════════════════════════'); + console.log('P4-API - 团课高级搜索测试结束'); + console.log('════════════════════════════════════════════'); + console.log(' 已测试:'); + console.log(` 1. 按类型筛选 (typeId=${typeId || 'N/A'})`); + console.log(` 2. 按标签筛选 (labelId=${labelId || 'N/A'})`); + console.log(` 3. 类型+标签组合筛选`); + console.log(' 4. 不存在类型/标签边界'); + console.log(' 5. 空关键词搜索'); + console.log('════════════════════════════════════════════'); + + expect(memberId).toBeDefined(); + expect(authToken).toBeTruthy(); + }); + }); +}); diff --git a/gym-manage-uniapp/e2e/specs/member-brand-notify.spec.js b/gym-manage-uniapp/e2e/specs/member-brand-notify.spec.js new file mode 100644 index 0000000..d9a58f8 --- /dev/null +++ b/gym-manage-uniapp/e2e/specs/member-brand-notify.spec.js @@ -0,0 +1,179 @@ +/** + * P3-API - 会员端品牌定制 & 通知消息 API 测试 + * 流程:会员登录 → 获取品牌配置 → 获取Banner/通知 + * + * 使用 HTTP API(HMAC-SHA256 签名 + JWT Token) + * 前置条件:后端服务 http://192.168.110.64:8084 已启动 + */ +const api = require('../helpers/api-helper'); + +let authToken = null; +let memberId = null; + +describe('P3-API - 会员端品牌定制 & 通知消息', () => { + // ════════════════════════════════════════════════════ + // 步骤0:会员登录 + // ════════════════════════════════════════════════════ + beforeAll(async () => { + console.log('════════════════════════════════════════════'); + console.log('P3-API - 品牌定制 & 通知消息测试'); + console.log('════════════════════════════════════════════'); + + const result = await api.memberLogin(); + expect(result).not.toBeNull(); + authToken = result.accessToken; + memberId = result.memberId; + console.log(`[P3-API] 会员登录成功: memberId=${memberId}`); + }, 30000); + + // ════════════════════════════════════════════════════ + // 步骤1:品牌定制配置 + // ════════════════════════════════════════════════════ + describe('品牌定制配置', () => { + test('TC-BRAND-001: 获取品牌配置', async () => { + const res = await api.getBrandConfig(authToken); + console.log(`[TC-BRAND-001] status=${res.status}`); + + if (api.isSuccess(res)) { + const brand = res.data?.data || res.data || {}; + console.log(` 品牌名称: ${brand.brandName || '(未设置)'}`); + console.log(` 品牌口号: ${brand.slogan || '(未设置)'}`); + console.log(` 主色调: ${brand.primaryColor || '(默认)'}`); + console.log(` 辅助色: ${brand.secondaryColor || '(默认)'}`); + console.log(` Logo URL: ${brand.logoUrl || '(默认)'}`); + console.log(` 背景URL: ${brand.backgroundUrl || '(默认)'}`); + + // 品牌名称应存在(至少默认值) + expect(brand).toBeDefined(); + } + }); + + test('TC-BRAND-002: 验证品牌颜色为合法HEX值', async () => { + const res = await api.getBrandConfig(authToken); + if (!api.isSuccess(res)) { + console.warn('[TC-BRAND-002] 获取品牌配置失败,跳过'); + return; + } + + const brand = res.data?.data || res.data || {}; + const primaryColor = brand.primaryColor; + const secondaryColor = brand.secondaryColor; + + if (primaryColor) { + // 验证是合法 HEX (#RRGGBB 或 #RRGGBBAA) + expect(/^#[0-9A-Fa-f]{6,8}$/.test(primaryColor)).toBe(true); + console.log(` 主色调 HEX 合法: ${primaryColor}`); + } + + if (secondaryColor) { + expect(/^#[0-9A-Fa-f]{6,8}$/.test(secondaryColor)).toBe(true); + console.log(` 辅助色 HEX 合法: ${secondaryColor}`); + } + }); + + test('TC-BRAND-003: Logo URL 应为有效路径或默认占位符', async () => { + const res = await api.getBrandConfig(authToken); + if (!api.isSuccess(res)) { + console.warn('[TC-BRAND-003] 获取品牌配置失败,跳过'); + return; + } + + const brand = res.data?.data || res.data || {}; + const logoUrl = brand.logoUrl; + + if (logoUrl) { + // 可以是完整URL、相对路径、或base64 + const isValid = typeof logoUrl === 'string' && logoUrl.length > 0; + expect(isValid).toBe(true); + console.log(` Logo URL: ${logoUrl.substring(0, 80)}...`); + } else { + console.log(' Logo URL 为空(使用默认值)'); + } + }); + }); + + // ════════════════════════════════════════════════════ + // 步骤2:Banner & 系统通知 + // ════════════════════════════════════════════════════ + describe('Banner & 系统通知', () => { + test('TC-NOTIFY-001: 获取活跃Banner列表', async () => { + const res = await api.getActiveBanners(authToken); + console.log(`[TC-NOTIFY-001] status=${res.status}`); + + if (api.isSuccess(res)) { + const banners = res.data?.data || res.data || []; + const records = Array.isArray(banners) ? banners : (banners.records || []); + console.log(` 活跃Banner数量: ${records.length}`); + + if (records.length > 0) { + const b = records[0]; + console.log(` 首条Banner: title="${b.title}", imageUrl=${(b.imageUrl || '').substring(0, 50)}`); + } + } + }); + + test('TC-NOTIFY-002: 获取系统通知列表', async () => { + const res = await api.getSystemNotices(authToken); + console.log(`[TC-NOTIFY-002] status=${res.status}`); + + if (api.isSuccess(res)) { + const notices = res.data?.data || res.data || []; + const records = Array.isArray(notices) ? notices : (notices.records || []); + console.log(` 系统通知数量: ${records.length}`); + + if (records.length > 0) { + const n = records[0]; + console.log(` 首条通知: title="${n.noticeTitle || n.title}", status=${n.status}`); + } + } + }); + + test('TC-NOTIFY-003: 获取用户消息列表', async () => { + const res = await api.getUserMessages(memberId, authToken); + console.log(`[TC-NOTIFY-003] status=${res.status}`); + + if (api.isSuccess(res)) { + const messages = res.data?.data || res.data || []; + const records = Array.isArray(messages) ? messages : (messages.records || []); + console.log(` 消息数量: ${records.length}`); + } + // 消息可能为空,不强制要求有数据 + }); + + test('TC-NOTIFY-004: 获取未读消息数', async () => { + const res = await api.getUnreadMessageCount(memberId, authToken); + console.log(`[TC-NOTIFY-004] status=${res.status}`); + + if (api.isSuccess(res)) { + const count = Number(res.data?.data ?? res.data ?? 0); + console.log(` 未读消息数: ${count}`); + expect(typeof count).toBe('number'); + expect(count).toBeGreaterThanOrEqual(0); + } + }); + }); + + // ════════════════════════════════════════════════════ + // 步骤3:总结 + // ════════════════════════════════════════════════════ + describe('总结', () => { + test('TC-P3-SUMMARY: 品牌定制 & 通知消息测试汇总', () => { + console.log(''); + console.log('════════════════════════════════════════════'); + console.log('P3-API - 品牌定制 & 通知消息测试结束'); + console.log('════════════════════════════════════════════'); + console.log(' 已测试:'); + console.log(' 1. 品牌配置获取 (名称/颜色/Logo/背景)'); + console.log(' 2. 颜色HEX格式校验'); + console.log(' 3. Logo URL有效性'); + console.log(' 4. 活跃Banner列表'); + console.log(' 5. 系统通知列表'); + console.log(' 6. 用户消息列表'); + console.log(' 7. 未读消息数量'); + console.log('════════════════════════════════════════════'); + + expect(memberId).toBeDefined(); + expect(authToken).toBeTruthy(); + }); + }); +}); diff --git a/gym-manage-web/e2e/journeys/brand-management-workflow.spec.ts b/gym-manage-web/e2e/journeys/brand-management-workflow.spec.ts new file mode 100644 index 0000000..9e15dbe --- /dev/null +++ b/gym-manage-web/e2e/journeys/brand-management-workflow.spec.ts @@ -0,0 +1,267 @@ +import { test, expect } from '@playwright/test'; + +test.describe('品牌定制管理完整工作流', () => { + test.describe.configure({ mode: 'serial' }); + + const testBrandName = `测试品牌_${Date.now()}`; + const testSlogan = `测试口号_${Date.now()}`; + const testPrimaryColor = '#FF5722'; + const testSecondaryColor = '#37474F'; + + test('品牌定制页面可正常加载', async ({ page }) => { + await test.step('导航到品牌定制页面', async () => { + await page.goto('/brand'); + await page.waitForLoadState('domcontentloaded'); + await page.waitForTimeout(1000); + }); + + await test.step('验证页面标题存在', async () => { + await expect(page.locator('h2')).toContainText('品牌定制', { timeout: 10000 }); + }); + + await test.step('验证四个配置卡片渲染', async () => { + // Logo 设置卡片 + await expect(page.locator('.brand-card').filter({ hasText: 'Logo 设置' })).toBeVisible({ timeout: 5000 }); + // 背景图设置卡片 + await expect(page.locator('.brand-card').filter({ hasText: '背景图设置' })).toBeVisible({ timeout: 5000 }); + // 品牌配色卡片 + await expect(page.locator('.brand-card').filter({ hasText: '品牌配色' })).toBeVisible({ timeout: 5000 }); + // 品牌信息卡片 + await expect(page.locator('.brand-card').filter({ hasText: '品牌信息' })).toBeVisible({ timeout: 5000 }); + }); + + await test.step('验证实时预览区域存在', async () => { + await expect(page.locator('.brand-card').filter({ hasText: '实时预览' })).toBeVisible({ timeout: 5000 }); + }); + + await test.step('验证 WebSocket 连接标签存在', async () => { + const wsTag = page.locator('.brand-page-header .el-tag'); + await expect(wsTag).toBeVisible({ timeout: 5000 }); + }); + }); + + test('更新品牌信息', async ({ page }) => { + await test.step('导航到品牌定制', async () => { + await page.goto('/brand'); + await page.waitForLoadState('domcontentloaded'); + await page.waitForTimeout(1500); + }); + + await test.step('填写品牌名称', async () => { + const brandCard = page.locator('.brand-card').filter({ hasText: '品牌信息' }); + const nameInput = brandCard.locator('input').first(); + await nameInput.clear(); + await nameInput.fill(testBrandName); + }); + + await test.step('填写品牌口号', async () => { + const brandCard = page.locator('.brand-card').filter({ hasText: '品牌信息' }); + const sloganInput = brandCard.locator('input').nth(1); + await sloganInput.clear(); + await sloganInput.fill(testSlogan); + }); + + await test.step('点击保存信息', async () => { + const brandCard = page.locator('.brand-card').filter({ hasText: '品牌信息' }); + await brandCard.locator('button:has-text("保存信息")').click(); + }); + + await test.step('验证保存成功提示', async () => { + await expect(page.locator('.el-message--success').first()).toBeVisible({ timeout: 8000 }); + }); + + await test.step('验证预览区域品牌名称更新', async () => { + await page.waitForTimeout(500); + const mockBrandName = page.locator('.mock-brand-name'); + await expect(mockBrandName).toContainText(testBrandName, { timeout: 5000 }); + }); + }); + + test('更新品牌配色', async ({ page }) => { + await test.step('导航到品牌定制', async () => { + await page.goto('/brand'); + await page.waitForLoadState('domcontentloaded'); + await page.waitForTimeout(1500); + }); + + await test.step('修改主色调', async () => { + const colorCard = page.locator('.brand-card').filter({ hasText: '品牌配色' }); + // 主色调 HEX 输入框(.color-input 是 el-input 容器,需定位内部 ) + const primaryHexInput = colorCard.locator('.color-row').first().locator('input').first(); + await primaryHexInput.clear(); + await primaryHexInput.fill(testPrimaryColor); + // 触发 blur 验证 + await primaryHexInput.blur(); + }); + + await test.step('修改辅助色', async () => { + const colorCard = page.locator('.brand-card').filter({ hasText: '品牌配色' }); + const secondaryHexInput = colorCard.locator('.color-row').nth(1).locator('input').first(); + await secondaryHexInput.clear(); + await secondaryHexInput.fill(testSecondaryColor); + await secondaryHexInput.blur(); + }); + + await test.step('保存配色', async () => { + const colorCard = page.locator('.brand-card').filter({ hasText: '品牌配色' }); + await colorCard.locator('button:has-text("保存配色")').click(); + }); + + await test.step('验证保存成功', async () => { + await expect(page.locator('.el-message--success').first()).toBeVisible({ timeout: 8000 }); + }); + }); + + test('品牌配色 - 输入非法HEX值应校验', async ({ page }) => { + await test.step('导航到品牌定制', async () => { + await page.goto('/brand'); + await page.waitForLoadState('domcontentloaded'); + await page.waitForTimeout(1500); + }); + + await test.step('输入无效HEX值并尝试保存', async () => { + const colorCard = page.locator('.brand-card').filter({ hasText: '品牌配色' }); + const primaryHexInput = colorCard.locator('.color-row').first().locator('input').first(); + await primaryHexInput.clear(); + await primaryHexInput.fill('not-a-color'); + await primaryHexInput.blur(); + + await colorCard.locator('button:has-text("保存配色")').click(); + }); + + await test.step('验证校验警告提示', async () => { + await expect(page.locator('.el-message--warning').first()).toBeVisible({ timeout: 5000 }); + }); + }); + + test('品牌信息 - 空名称校验', async ({ page }) => { + await test.step('导航到品牌定制', async () => { + await page.goto('/brand'); + await page.waitForLoadState('domcontentloaded'); + await page.waitForTimeout(1500); + }); + + await test.step('清空品牌名称并保存', async () => { + const brandCard = page.locator('.brand-card').filter({ hasText: '品牌信息' }); + const nameInput = brandCard.locator('input').first(); + await nameInput.clear(); + await brandCard.locator('button:has-text("保存信息")').click(); + }); + + await test.step('验证警告提示', async () => { + await expect(page.locator('.el-message--warning').first()).toBeVisible({ timeout: 5000 }); + }); + }); + + test('Logo 区域 - 上传组件正常渲染', async ({ page }) => { + await test.step('导航到品牌定制', async () => { + await page.goto('/brand'); + await page.waitForLoadState('domcontentloaded'); + await page.waitForTimeout(1500); + }); + + await test.step('验证 Logo 上传区域存在', async () => { + const logoCard = page.locator('.brand-card').filter({ hasText: 'Logo 设置' }); + await expect(logoCard.locator('.el-upload')).toBeVisible({ timeout: 5000 }); + }); + }); + + test('背景图区域 - 上传组件正常渲染', async ({ page }) => { + await test.step('导航到品牌定制', async () => { + await page.goto('/brand'); + await page.waitForLoadState('domcontentloaded'); + await page.waitForTimeout(1500); + }); + + await test.step('验证背景图上传区域存在', async () => { + const bgCard = page.locator('.brand-card').filter({ hasText: '背景图设置' }); + await expect(bgCard.locator('.el-upload')).toBeVisible({ timeout: 5000 }); + }); + }); + + test('实时预览 - 底部导航切换', async ({ page }) => { + await test.step('导航到品牌定制', async () => { + await page.goto('/brand'); + await page.waitForLoadState('domcontentloaded'); + await page.waitForTimeout(1500); + }); + + await test.step('验证默认首页可见', async () => { + await expect(page.locator('.mock-brand-section')).toBeVisible({ timeout: 5000 }); + await expect(page.locator('.mock-greeting-card')).toBeVisible({ timeout: 5000 }); + }); + + await test.step('切换到课程tab', async () => { + await page.locator('.mock-tab-item').nth(1).click(); + await page.waitForTimeout(300); + await expect(page.locator('.mock-search-area')).toBeVisible({ timeout: 5000 }); + }); + + await test.step('切换到我的课程tab', async () => { + await page.locator('.mock-tab-item').nth(2).click(); + await page.waitForTimeout(300); + await expect(page.locator('.mock-booking-card').first()).toBeVisible({ timeout: 5000 }); + }); + + await test.step('切换到我的tab', async () => { + await page.locator('.mock-tab-item').nth(3).click(); + await page.waitForTimeout(300); + await expect(page.locator('.mock-profile-card')).toBeVisible({ timeout: 5000 }); + }); + + await test.step('切换回首页', async () => { + await page.locator('.mock-tab-item').first().click(); + await page.waitForTimeout(300); + await expect(page.locator('.mock-greeting-card')).toBeVisible({ timeout: 5000 }); + }); + }); + + test('实时预览 - 课程详情钻取与返回', async ({ page }) => { + await test.step('导航到品牌定制', async () => { + await page.goto('/brand'); + await page.waitForLoadState('domcontentloaded'); + await page.waitForTimeout(1500); + }); + + await test.step('点击今日推荐课程', async () => { + await page.locator('.mock-course-card').first().click(); + await page.waitForTimeout(300); + }); + + await test.step('验证课程详情页渲染', async () => { + await expect(page.locator('.mock-detail-header')).toBeVisible({ timeout: 5000 }); + await expect(page.locator('.mock-detail-title')).toBeVisible({ timeout: 5000 }); + }); + + await test.step('验证详情信息卡片', async () => { + await expect(page.locator('.mock-detail-info')).toBeVisible({ timeout: 5000 }); + }); + + await test.step('点击返回按钮', async () => { + await page.locator('.mock-back-btn').click(); + await page.waitForTimeout(300); + }); + + await test.step('验证返回首页', async () => { + await expect(page.locator('.mock-greeting-card')).toBeVisible({ timeout: 5000 }); + }); + }); + + test('实时预览 - 今日推荐查看更多进入搜索', async ({ page }) => { + await test.step('导航到品牌定制', async () => { + await page.goto('/brand'); + await page.waitForLoadState('domcontentloaded'); + await page.waitForTimeout(1500); + }); + + await test.step('点击查看全部', async () => { + await page.locator('.mock-section-more').click(); + await page.waitForTimeout(300); + }); + + await test.step('验证进入课程搜索页', async () => { + await expect(page.locator('.mock-search-area')).toBeVisible({ timeout: 5000 }); + await expect(page.locator('.mock-search-list')).toBeVisible({ timeout: 5000 }); + }); + }); +}); diff --git a/gym-manage-web/playwright/.auth/user.json b/gym-manage-web/playwright/.auth/user.json index d9c595d..9a4b918 100644 --- a/gym-manage-web/playwright/.auth/user.json +++ b/gym-manage-web/playwright/.auth/user.json @@ -6,11 +6,11 @@ "localStorage": [ { "name": "token", - "value": "eyJhbGciOiJIUzM4NCJ9.eyJyb2xlcyI6WyJhZG1pbiJdLCJ1c2VySWQiOjEsInVzZXJuYW1lIjoiYWRtaW4iLCJzdWIiOiJhZG1pbiIsImlhdCI6MTc4NDcxODUyNiwiZXhwIjoxNzg0ODA0OTI2fQ.WQY3IoWxEIT7Qn0xQft2uZjiItcasuAxF297MCaCHYbQXebCClJUoz9Y4_UicE2i" + "value": "eyJhbGciOiJIUzM4NCJ9.eyJyb2xlcyI6WyJhZG1pbiJdLCJ0ZW5hbnRJZCI6ImRlZmF1bHQiLCJ1c2VySWQiOjEsInVzZXJuYW1lIjoiYWRtaW4iLCJzdWIiOiJhZG1pbiIsImlhdCI6MTc4NDgwODI0OCwiZXhwIjoxNzg0ODk0NjQ4fQ.HMmMZFVaboJrr0qk3LwhwINrqANBLFmc3YWBIMUakNqRrPkGQMFnveOI96pOX67R" }, { "name": "permission", - "value": "{\"roles\":[\"admin\"],\"permissions\":[\"system:user:list\",\"system:role:list\",\"system:menu:list\",\"system:dept:list\",\"system:dict:list\",\"system:config:list\",\"system:notice:list\",\"system:file:list\",\"system:user:query\",\"system:user:add\",\"system:user:edit\",\"system:user:remove\",\"system:user:export\",\"system:user:import\",\"system:user:resetPwd\",\"system:role:query\",\"system:role:add\",\"system:role:edit\",\"system:role:remove\",\"system:role:export\",\"system:menu:query\",\"system:menu:add\",\"system:menu:edit\",\"system:menu:remove\",\"audit:operation:list\",\"audit:login:list\",\"audit:exception:list\",\"audit:operation:query\",\"audit:operation:remove\",\"audit:operation:export\",\"audit:login:query\",\"audit:login:remove\",\"audit:login:export\",\"audit:exception:query\",\"audit:exception:remove\",\"audit:exception:export\",\"monitor:online:list\",\"monitor:job:list\",\"monitor:data:list\",\"monitor:server:list\",\"monitor:cache:list\",\"monitor:online:query\",\"monitor:online:forceLogout\",\"monitor:job:query\",\"monitor:job:add\",\"monitor:job:edit\",\"monitor:job:remove\",\"monitor:job:execute\",\"business:groupCourse:list\",\"business:groupCourseType:list\",\"business:groupCourseLabel:list\",\"business:groupCourseRecommend:list\",\"business:groupCourse:query\",\"business:groupCourse:add\",\"business:groupCourse:edit\",\"business:groupCourse:remove\",\"business:groupCourse:cancel\",\"business:groupCourseType:query\",\"business:groupCourseType:add\",\"business:groupCourseType:edit\",\"business:groupCourseType:remove\",\"business:groupCourseLabel:query\",\"business:groupCourseLabel:add\",\"business:groupCourseLabel:edit\",\"business:groupCourseLabel:remove\",\"business:groupCourseRecommend:query\",\"business:groupCourseRecommend:add\",\"business:groupCourseRecommend:edit\",\"business:groupCourseRecommend:remove\",\"business:member:list\",\"business:memberCard:list\",\"business:statistics:view\",\"business:member:query\",\"business:member:add\",\"business:member:edit\",\"business:member:remove\",\"business:member:detail\",\"business:memberCard:query\",\"business:memberCard:add\",\"business:memberCard:edit\",\"business:memberCard:remove\",\"business:statistics:export\",\"system:banner:query\",\"system:banner:add\",\"system:banner:edit\",\"system:banner:remove\",\"system:banner:list\",\"system:coach:list\",\"system:coach:query\",\"system:coach:add\",\"system:coach:edit\",\"system:coach:remove\"],\"menus\":[{\"id\":\"1\",\"name\":\"系统管理\",\"path\":\"\",\"icon\":\"Setting\",\"sort\":1,\"children\":[{\"id\":\"11\",\"name\":\"用户管理\",\"path\":\"/users\",\"icon\":\"User\",\"parentId\":\"1\",\"sort\":1},{\"id\":\"12\",\"name\":\"角色管理\",\"path\":\"/roles\",\"icon\":\"UserFilled\",\"parentId\":\"1\",\"sort\":2},{\"id\":\"13\",\"name\":\"菜单管理\",\"path\":\"/menus\",\"icon\":\"Menu\",\"parentId\":\"1\",\"sort\":3},{\"id\":\"14\",\"name\":\"部门管理\",\"path\":\"/dept\",\"icon\":\"Document\",\"parentId\":\"1\",\"sort\":4},{\"id\":\"15\",\"name\":\"字典管理\",\"path\":\"/dict\",\"icon\":\"Collection\",\"parentId\":\"1\",\"sort\":5},{\"id\":\"16\",\"name\":\"参数管理\",\"path\":\"/sys/config\",\"icon\":\"Document\",\"parentId\":\"1\",\"sort\":6},{\"id\":\"17\",\"name\":\"通知公告\",\"path\":\"/notice\",\"icon\":\"Bell\",\"parentId\":\"1\",\"sort\":7},{\"id\":\"18\",\"name\":\"文件管理\",\"path\":\"/files\",\"icon\":\"Folder\",\"parentId\":\"1\",\"sort\":8}]},{\"id\":\"2\",\"name\":\"审计日志\",\"path\":\"\",\"icon\":\"Document\",\"sort\":2,\"children\":[{\"id\":\"21\",\"name\":\"操作日志\",\"path\":\"/oplog\",\"icon\":\"Document\",\"parentId\":\"2\",\"sort\":1},{\"id\":\"22\",\"name\":\"登录日志\",\"path\":\"/loginlog\",\"icon\":\"Document\",\"parentId\":\"2\",\"sort\":2},{\"id\":\"23\",\"name\":\"异常日志\",\"path\":\"/exceptionlog\",\"icon\":\"Warning\",\"parentId\":\"2\",\"sort\":3}]},{\"id\":\"3\",\"name\":\"系统监控\",\"path\":\"\",\"icon\":\"Monitor\",\"sort\":3,\"children\":[{\"id\":\"31\",\"name\":\"在线用户\",\"path\":\"/monitor/online\",\"icon\":\"Document\",\"parentId\":\"3\",\"sort\":1},{\"id\":\"32\",\"name\":\"定时任务\",\"path\":\"/monitor/job\",\"icon\":\"Document\",\"parentId\":\"3\",\"sort\":2},{\"id\":\"33\",\"name\":\"数据监控\",\"path\":\"/monitor/data\",\"icon\":\"Document\",\"parentId\":\"3\",\"sort\":3},{\"id\":\"34\",\"name\":\"服务监控\",\"path\":\"/monitor/server\",\"icon\":\"Document\",\"parentId\":\"3\",\"sort\":4},{\"id\":\"35\",\"name\":\"缓存监控\",\"path\":\"/monitor/cache\",\"icon\":\"Document\",\"parentId\":\"3\",\"sort\":5}]},{\"id\":\"400\",\"name\":\"团课管理\",\"path\":\"\",\"icon\":\"TrophyBase\",\"sort\":4,\"children\":[{\"id\":\"401\",\"name\":\"团课管理\",\"path\":\"/courses\",\"icon\":\"TrophyBase\",\"parentId\":\"400\",\"sort\":1},{\"id\":\"402\",\"name\":\"团课类型\",\"path\":\"/courses/types\",\"icon\":\"Grid\",\"parentId\":\"400\",\"sort\":2},{\"id\":\"403\",\"name\":\"类型标签\",\"path\":\"/courses/labels\",\"icon\":\"Document\",\"parentId\":\"400\",\"sort\":3},{\"id\":\"404\",\"name\":\"团课推荐\",\"path\":\"/courses/recommend\",\"icon\":\"Star\",\"parentId\":\"400\",\"sort\":4}]},{\"id\":\"500\",\"name\":\"会员管理\",\"path\":\"\",\"icon\":\"Avatar\",\"sort\":5,\"children\":[{\"id\":\"501\",\"name\":\"会员管理\",\"path\":\"/members\",\"icon\":\"Avatar\",\"parentId\":\"500\",\"sort\":1}]},{\"id\":\"502\",\"name\":\"会员卡管理\",\"path\":\"\",\"icon\":\"CreditCard\",\"sort\":6,\"children\":[{\"id\":\"503\",\"name\":\"会员卡管理\",\"path\":\"/member-cards\",\"icon\":\"CreditCard\",\"parentId\":\"502\",\"sort\":1}]},{\"id\":\"504\",\"name\":\"数据统计\",\"path\":\"\",\"icon\":\"DataAnalysis\",\"sort\":7,\"children\":[{\"id\":\"505\",\"name\":\"数据统计看板\",\"path\":\"/statistics\",\"icon\":\"Document\",\"parentId\":\"504\",\"sort\":1}]},{\"id\":\"19\",\"name\":\"轮播图管理\",\"path\":\"/banners\",\"icon\":\"Picture\",\"sort\":8},{\"id\":\"20\",\"name\":\"教练管理\",\"path\":\"/coach\",\"icon\":\"Medal\",\"sort\":9}]}" + "value": "{\"roles\":[\"admin\"],\"permissions\":[\"system:user:list\",\"system:role:list\",\"system:menu:list\",\"system:dept:list\",\"system:dict:list\",\"system:config:list\",\"system:notice:list\",\"system:file:list\",\"system:user:query\",\"system:user:add\",\"system:user:edit\",\"system:user:remove\",\"system:user:export\",\"system:user:import\",\"system:user:resetPwd\",\"system:role:query\",\"system:role:add\",\"system:role:edit\",\"system:role:remove\",\"system:role:export\",\"system:menu:query\",\"system:menu:add\",\"system:menu:edit\",\"system:menu:remove\",\"audit:operation:list\",\"audit:login:list\",\"audit:exception:list\",\"audit:operation:query\",\"audit:operation:remove\",\"audit:operation:export\",\"audit:login:query\",\"audit:login:remove\",\"audit:login:export\",\"audit:exception:query\",\"audit:exception:remove\",\"audit:exception:export\",\"monitor:online:list\",\"monitor:job:list\",\"monitor:data:list\",\"monitor:server:list\",\"monitor:cache:list\",\"monitor:online:query\",\"monitor:online:forceLogout\",\"monitor:job:query\",\"monitor:job:add\",\"monitor:job:edit\",\"monitor:job:remove\",\"monitor:job:execute\",\"business:groupCourse:list\",\"business:groupCourseType:list\",\"business:groupCourseLabel:list\",\"business:groupCourseRecommend:list\",\"business:groupCourse:query\",\"business:groupCourse:add\",\"business:groupCourse:edit\",\"business:groupCourse:remove\",\"business:groupCourse:cancel\",\"business:groupCourseType:query\",\"business:groupCourseType:add\",\"business:groupCourseType:edit\",\"business:groupCourseType:remove\",\"business:groupCourseLabel:query\",\"business:groupCourseLabel:add\",\"business:groupCourseLabel:edit\",\"business:groupCourseLabel:remove\",\"business:groupCourseRecommend:query\",\"business:groupCourseRecommend:add\",\"business:groupCourseRecommend:edit\",\"business:groupCourseRecommend:remove\",\"business:member:list\",\"business:memberCard:list\",\"business:statistics:view\",\"business:member:query\",\"business:member:add\",\"business:member:edit\",\"business:member:remove\",\"business:member:detail\",\"business:memberCard:query\",\"business:memberCard:add\",\"business:memberCard:edit\",\"business:memberCard:remove\",\"business:statistics:export\",\"system:banner:query\",\"system:banner:add\",\"system:banner:edit\",\"system:banner:remove\",\"system:banner:list\",\"system:coach:list\",\"system:coach:query\",\"system:coach:add\",\"system:coach:edit\",\"system:coach:remove\",\"brand:config:list\",\"brand:config:query\",\"brand:config:edit\",\"brand:logo:upload\",\"brand:bg:upload\"],\"menus\":[{\"id\":\"1\",\"name\":\"系统管理\",\"path\":\"\",\"icon\":\"Setting\",\"sort\":1,\"children\":[{\"id\":\"11\",\"name\":\"用户管理\",\"path\":\"/users\",\"icon\":\"User\",\"parentId\":\"1\",\"sort\":1},{\"id\":\"12\",\"name\":\"角色管理\",\"path\":\"/roles\",\"icon\":\"UserFilled\",\"parentId\":\"1\",\"sort\":2},{\"id\":\"13\",\"name\":\"菜单管理\",\"path\":\"/menus\",\"icon\":\"Menu\",\"parentId\":\"1\",\"sort\":3},{\"id\":\"14\",\"name\":\"部门管理\",\"path\":\"/dept\",\"icon\":\"Document\",\"parentId\":\"1\",\"sort\":4},{\"id\":\"15\",\"name\":\"字典管理\",\"path\":\"/dict\",\"icon\":\"Collection\",\"parentId\":\"1\",\"sort\":5},{\"id\":\"16\",\"name\":\"参数管理\",\"path\":\"/sys/config\",\"icon\":\"Document\",\"parentId\":\"1\",\"sort\":6},{\"id\":\"17\",\"name\":\"通知公告\",\"path\":\"/notice\",\"icon\":\"Bell\",\"parentId\":\"1\",\"sort\":7},{\"id\":\"18\",\"name\":\"文件管理\",\"path\":\"/files\",\"icon\":\"Folder\",\"parentId\":\"1\",\"sort\":8}]},{\"id\":\"2\",\"name\":\"审计日志\",\"path\":\"\",\"icon\":\"Document\",\"sort\":2,\"children\":[{\"id\":\"21\",\"name\":\"操作日志\",\"path\":\"/oplog\",\"icon\":\"Document\",\"parentId\":\"2\",\"sort\":1},{\"id\":\"22\",\"name\":\"登录日志\",\"path\":\"/loginlog\",\"icon\":\"Document\",\"parentId\":\"2\",\"sort\":2},{\"id\":\"23\",\"name\":\"异常日志\",\"path\":\"/exceptionlog\",\"icon\":\"Warning\",\"parentId\":\"2\",\"sort\":3}]},{\"id\":\"3\",\"name\":\"系统监控\",\"path\":\"\",\"icon\":\"Monitor\",\"sort\":3,\"children\":[{\"id\":\"31\",\"name\":\"在线用户\",\"path\":\"/monitor/online\",\"icon\":\"Document\",\"parentId\":\"3\",\"sort\":1},{\"id\":\"32\",\"name\":\"定时任务\",\"path\":\"/monitor/job\",\"icon\":\"Document\",\"parentId\":\"3\",\"sort\":2},{\"id\":\"33\",\"name\":\"数据监控\",\"path\":\"/monitor/data\",\"icon\":\"Document\",\"parentId\":\"3\",\"sort\":3},{\"id\":\"34\",\"name\":\"服务监控\",\"path\":\"/monitor/server\",\"icon\":\"Document\",\"parentId\":\"3\",\"sort\":4},{\"id\":\"35\",\"name\":\"缓存监控\",\"path\":\"/monitor/cache\",\"icon\":\"Document\",\"parentId\":\"3\",\"sort\":5}]},{\"id\":\"400\",\"name\":\"团课管理\",\"path\":\"\",\"icon\":\"TrophyBase\",\"sort\":4,\"children\":[{\"id\":\"401\",\"name\":\"团课管理\",\"path\":\"/courses\",\"icon\":\"TrophyBase\",\"parentId\":\"400\",\"sort\":1},{\"id\":\"402\",\"name\":\"团课类型\",\"path\":\"/courses/types\",\"icon\":\"Grid\",\"parentId\":\"400\",\"sort\":2},{\"id\":\"403\",\"name\":\"类型标签\",\"path\":\"/courses/labels\",\"icon\":\"Document\",\"parentId\":\"400\",\"sort\":3},{\"id\":\"404\",\"name\":\"团课推荐\",\"path\":\"/courses/recommend\",\"icon\":\"Star\",\"parentId\":\"400\",\"sort\":4}]},{\"id\":\"500\",\"name\":\"会员管理\",\"path\":\"\",\"icon\":\"Avatar\",\"sort\":5,\"children\":[{\"id\":\"501\",\"name\":\"会员管理\",\"path\":\"/members\",\"icon\":\"Avatar\",\"parentId\":\"500\",\"sort\":1}]},{\"id\":\"502\",\"name\":\"会员卡管理\",\"path\":\"\",\"icon\":\"CreditCard\",\"sort\":6,\"children\":[{\"id\":\"503\",\"name\":\"会员卡管理\",\"path\":\"/member-cards\",\"icon\":\"CreditCard\",\"parentId\":\"502\",\"sort\":1}]},{\"id\":\"504\",\"name\":\"数据统计\",\"path\":\"\",\"icon\":\"DataAnalysis\",\"sort\":7,\"children\":[{\"id\":\"505\",\"name\":\"数据统计看板\",\"path\":\"/statistics\",\"icon\":\"Document\",\"parentId\":\"504\",\"sort\":1}]},{\"id\":\"19\",\"name\":\"轮播图管理\",\"path\":\"/banners\",\"icon\":\"Picture\",\"sort\":8},{\"id\":\"20\",\"name\":\"教练管理\",\"path\":\"/coach\",\"icon\":\"Medal\",\"sort\":9},{\"id\":\"29\",\"name\":\"品牌定制\",\"path\":\"/brand\",\"icon\":\"Brush\",\"sort\":9}]}" }, { "name": "userId", diff --git a/gym-manage-web/src/__tests__/api/banner.api.test.ts b/gym-manage-web/src/__tests__/api/banner.api.test.ts new file mode 100644 index 0000000..32aa9b6 --- /dev/null +++ b/gym-manage-web/src/__tests__/api/banner.api.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const { mockRequest } = vi.hoisted(() => ({ + mockRequest: { + get: vi.fn().mockResolvedValue({}), + post: vi.fn().mockResolvedValue({}), + put: vi.fn().mockResolvedValue({}), + delete: vi.fn().mockResolvedValue({}), + } +})) + +vi.mock('@/utils/request', () => ({ default: mockRequest })) + +import { bannerApi } from '@/api/banner.api' + +describe('bannerApi', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('getAll', () => { + it('should call GET /banners', async () => { + await bannerApi.getAll() + expect(mockRequest.get).toHaveBeenCalledWith('/banners') + }) + }) + + describe('getActive', () => { + it('should call GET /banners/active', async () => { + await bannerApi.getActive() + expect(mockRequest.get).toHaveBeenCalledWith('/banners/active') + }) + }) + + describe('getById', () => { + it('should call GET /banners/:id', async () => { + await bannerApi.getById(1) + expect(mockRequest.get).toHaveBeenCalledWith('/banners/1') + }) + }) + + describe('create', () => { + it('should call POST /banners with data', async () => { + const data = { imageUrl: 'https://example.com/banner.jpg', title: 'New Banner' } + await bannerApi.create(data) + expect(mockRequest.post).toHaveBeenCalledWith('/banners', data) + }) + }) + + describe('update', () => { + it('should call PUT /banners/:id with data', async () => { + const data = { imageUrl: 'https://example.com/banner.jpg', title: 'Updated' } + await bannerApi.update(1, data) + expect(mockRequest.put).toHaveBeenCalledWith('/banners/1', data) + }) + }) + + describe('delete', () => { + it('should call DELETE /banners/:id', async () => { + await bannerApi.delete(1) + expect(mockRequest.delete).toHaveBeenCalledWith('/banners/1') + }) + }) +}) diff --git a/gym-manage-web/src/__tests__/api/coach.api.test.ts b/gym-manage-web/src/__tests__/api/coach.api.test.ts new file mode 100644 index 0000000..3749d8a --- /dev/null +++ b/gym-manage-web/src/__tests__/api/coach.api.test.ts @@ -0,0 +1,71 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const { mockRequest } = vi.hoisted(() => ({ + mockRequest: { + get: vi.fn().mockResolvedValue({}), + post: vi.fn().mockResolvedValue({}), + put: vi.fn().mockResolvedValue({}), + delete: vi.fn().mockResolvedValue({}), + } +})) + +vi.mock('@/utils/request', () => ({ default: mockRequest })) + +import { coachApi } from '@/api/coach.api' + +describe('coachApi', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('getAll', () => { + it('should call GET /coach/list', async () => { + await coachApi.getAll() + expect(mockRequest.get).toHaveBeenCalledWith('/coach/list') + }) + }) + + describe('create', () => { + it('should call POST /coach with data', async () => { + const data = { username: 'coach1', password: 'pass', nickname: 'Coach One', email: 'c@test.com', phone: '123' } + await coachApi.create(data) + expect(mockRequest.post).toHaveBeenCalledWith('/coach', data) + }) + }) + + describe('update', () => { + it('should call PUT /coach/:id with data', async () => { + const data = { nickname: 'New Name' } + await coachApi.update('1', data) + expect(mockRequest.put).toHaveBeenCalledWith('/coach/1', data) + }) + }) + + describe('disable', () => { + it('should call POST /coach/:id/disable', async () => { + await coachApi.disable('1') + expect(mockRequest.post).toHaveBeenCalledWith('/coach/1/disable') + }) + }) + + describe('getCourses', () => { + it('should call GET /coach/:id/courses', async () => { + await coachApi.getCourses('1') + expect(mockRequest.get).toHaveBeenCalledWith('/coach/1/courses') + }) + }) + + describe('getViolationCounts', () => { + it('should call GET /coach/violation-counts', async () => { + await coachApi.getViolationCounts() + expect(mockRequest.get).toHaveBeenCalledWith('/coach/violation-counts') + }) + }) + + describe('getViolations', () => { + it('should call GET /coach/:id/violations', async () => { + await coachApi.getViolations('1') + expect(mockRequest.get).toHaveBeenCalledWith('/coach/1/violations') + }) + }) +}) diff --git a/gym-manage-web/src/__tests__/api/memberCard.api.test.ts b/gym-manage-web/src/__tests__/api/memberCard.api.test.ts new file mode 100644 index 0000000..76f3e0c --- /dev/null +++ b/gym-manage-web/src/__tests__/api/memberCard.api.test.ts @@ -0,0 +1,76 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const { mockRequest } = vi.hoisted(() => ({ + mockRequest: { + get: vi.fn().mockResolvedValue({}), + post: vi.fn().mockResolvedValue({}), + put: vi.fn().mockResolvedValue({}), + delete: vi.fn().mockResolvedValue({}), + } +})) + +vi.mock('@/utils/request', () => ({ default: mockRequest })) + +import { memberCardApi } from '@/api/memberCard.api' + +describe('memberCardApi', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('list', () => { + it('should call GET /member-cards with params', async () => { + const params = { page: 0, size: 10 } + await memberCardApi.list(params) + expect(mockRequest.get).toHaveBeenCalledWith('/member-cards', { params }) + }) + + it('should include filter params', async () => { + const params = { status: 1, name: 'Gold', page: 0, size: 20 } + await memberCardApi.list(params) + expect(mockRequest.get).toHaveBeenCalledWith('/member-cards', { params }) + }) + }) + + describe('getActiveCards', () => { + it('should call GET /member-cards/active', async () => { + await memberCardApi.getActiveCards() + expect(mockRequest.get).toHaveBeenCalledWith('/member-cards/active', { params: { status: undefined } }) + }) + + it('should pass status param', async () => { + await memberCardApi.getActiveCards(1) + expect(mockRequest.get).toHaveBeenCalledWith('/member-cards/active', { params: { status: 1 } }) + }) + }) + + describe('getById', () => { + it('should call GET /member-cards/:id', async () => { + await memberCardApi.getById(5) + expect(mockRequest.get).toHaveBeenCalledWith('/member-cards/5') + }) + }) + + describe('create', () => { + it('should call POST /member-cards with data', async () => { + const data = { memberCardName: 'Gold', memberCardPrice: 199 } + await memberCardApi.create(data) + expect(mockRequest.post).toHaveBeenCalledWith('/member-cards', data) + }) + }) + + describe('update', () => { + it('should call PUT /member-cards/:id with data', async () => { + const data = { memberCardName: 'Platinum' } + await memberCardApi.update(3, data) + expect(mockRequest.put).toHaveBeenCalledWith('/member-cards/3', data) + }) + }) + + describe('delete', () => { + it('should call DELETE /member-cards/:id', async () => { + await memberCardApi.delete(3) + expect(mockRequest.delete).toHaveBeenCalledWith('/member-cards/3') + }) + }) +}) diff --git a/gym-manage-web/src/__tests__/components/BannerManagement.test.ts b/gym-manage-web/src/__tests__/components/BannerManagement.test.ts new file mode 100644 index 0000000..45ff438 --- /dev/null +++ b/gym-manage-web/src/__tests__/components/BannerManagement.test.ts @@ -0,0 +1,371 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' +import { mount } from '@vue/test-utils' +import { createRouter, createMemoryHistory } from 'vue-router' +import { createPinia, setActivePinia } from 'pinia' +import BannerManagement from '@/views/banner/BannerManagement.vue' + +// ===== Mock Element Plus ===== +vi.mock('element-plus', () => ({ + ElMessage: { success: vi.fn(), error: vi.fn(), warning: vi.fn() }, + ElMessageBox: { confirm: vi.fn() }, +})) + +// ===== Mock @element-plus/icons-vue ===== +vi.mock('@element-plus/icons-vue', () => ({ + Plus: { template: '' }, +})) + +// ===== Mock banner.api ===== +vi.mock('@/api/banner.api', () => ({ + bannerApi: { + getAll: vi.fn().mockResolvedValue([ + { id: 1, imageUrl: '/api/files/1/preview', title: '轮播图1', subtitle: '副标题1', sortOrder: 1, isActive: true, createdAt: '2025-01-01T00:00:00' }, + { id: 2, imageUrl: '/api/files/2/preview', title: '轮播图2', subtitle: '副标题2', sortOrder: 2, isActive: false, createdAt: '2025-01-02T00:00:00' }, + ]), + create: vi.fn().mockResolvedValue({}), + update: vi.fn().mockResolvedValue({}), + delete: vi.fn().mockResolvedValue({}), + }, +})) + +// ===== Mock request ===== +vi.mock('@/utils/request', () => { + const mockRequest = { + get: vi.fn().mockResolvedValue({}), + post: vi.fn().mockResolvedValue({}), + put: vi.fn().mockResolvedValue({}), + delete: vi.fn().mockResolvedValue({}), + } + return { default: mockRequest } +}) + +// ===== Mock dateFormat ===== +vi.mock('@/utils/dateFormat', () => ({ + formatDateTime: vi.fn((val: string) => val || '-'), +})) + +// ===== Mock URL.createObjectURL ===== +if (!globalThis.URL.createObjectURL) { + // @ts-ignore + globalThis.URL.createObjectURL = vi.fn(() => `blob:mock-${Math.random()}`) + // @ts-ignore + globalThis.URL.revokeObjectURL = vi.fn() +} + +describe('BannerManagement Component', () => { + let router: any + let wrapper: any + + const defaultStubs = { + 'el-button': { template: '' }, + 'el-card': { template: '
' }, + 'el-input': { template: '', props: ['modelValue'], emits: ['update:modelValue'] }, + 'el-icon': { template: '' }, + 'el-table': { template: '
', props: ['data', 'loading'] }, + 'el-table-column': true, + 'el-tag': { template: '', props: ['type', 'effect', 'size'] }, + 'el-dialog': { template: '
', props: ['modelValue', 'title', 'width'] }, + 'el-form': { template: '
', props: ['model', 'rules', 'label-width', 'ref'] }, + 'el-form-item': { template: '
', props: ['label', 'prop'] }, + 'el-upload': { template: '
', props: ['http-request', 'show-file-list', 'before-upload', 'accept'] }, + 'el-input-number': true, + 'el-switch': true, + 'el-image': true, + } + + beforeEach(() => { + const pinia = createPinia() + setActivePinia(pinia) + + router = createRouter({ + history: createMemoryHistory(), + routes: [{ path: '/', component: { template: '
' } }], + }) + + vi.clearAllMocks() + }) + + afterEach(() => { + if (wrapper) wrapper.unmount() + }) + + // ==================== 组件初始化 ==================== + + it('应该渲染轮播图管理容器', async () => { + wrapper = mount(BannerManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.find('.banner-management').exists()).toBe(true) + }) + + it('应该渲染页面标题', async () => { + wrapper = mount(BannerManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.html()).toContain('轮播图管理') + }) + + it('应该渲染新增轮播图按钮', async () => { + wrapper = mount(BannerManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.html()).toContain('新增轮播图') + }) + + // ==================== 初始状态 ==================== + + it('初始 loading 状态为 false', async () => { + wrapper = mount(BannerManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.vm.loading).toBe(false) + }) + + it('初始 modalVisible 为 false', async () => { + wrapper = mount(BannerManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.vm.modalVisible).toBe(false) + }) + + it('初始 dataSource 为空数组', async () => { + wrapper = mount(BannerManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.vm.dataSource).toBeDefined() + expect(Array.isArray(wrapper.vm.dataSource)).toBe(true) + }) + + // ==================== 方法存在性 ==================== + + it('应该暴露 handleAdd 方法', async () => { + wrapper = mount(BannerManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(typeof wrapper.vm.handleAdd).toBe('function') + }) + + it('应该暴露 handleEdit 方法', async () => { + wrapper = mount(BannerManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(typeof wrapper.vm.handleEdit).toBe('function') + }) + + it('应该暴露 handleDelete 方法', async () => { + wrapper = mount(BannerManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(typeof wrapper.vm.handleDelete).toBe('function') + }) + + it('应该暴露 handleUpload 方法', async () => { + wrapper = mount(BannerManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(typeof wrapper.vm.handleUpload).toBe('function') + }) + + it('应该暴露 beforeUpload 方法', async () => { + wrapper = mount(BannerManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(typeof wrapper.vm.beforeUpload).toBe('function') + }) + + // ==================== 新增/编辑表单 ==================== + + it('点击新增后 modalVisible 应为 true', async () => { + wrapper = mount(BannerManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + wrapper.vm.handleAdd() + await wrapper.vm.$nextTick() + + expect(wrapper.vm.modalVisible).toBe(true) + expect(wrapper.vm.modalTitle).toBe('新增轮播图') + }) + + it('新增时表单 title 应为空', async () => { + wrapper = mount(BannerManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + wrapper.vm.handleAdd() + await wrapper.vm.$nextTick() + + expect(wrapper.vm.formState.title).toBe('') + }) + + it('新增时 sortOrder 应为 0', async () => { + wrapper = mount(BannerManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + wrapper.vm.handleAdd() + await wrapper.vm.$nextTick() + + expect(wrapper.vm.formState.sortOrder).toBe(0) + }) + + it('新增时 isActive 应为 true', async () => { + wrapper = mount(BannerManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + wrapper.vm.handleAdd() + await wrapper.vm.$nextTick() + + expect(wrapper.vm.formState.isActive).toBe(true) + }) + + // ==================== 表单规则 ==================== + + it('应该定义 imageUrl 必填规则', async () => { + wrapper = mount(BannerManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.vm.formRules.imageUrl).toBeDefined() + expect(wrapper.vm.formRules.imageUrl[0].required).toBe(true) + }) + + it('应该定义 title 必填规则', async () => { + wrapper = mount(BannerManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.vm.formRules.title).toBeDefined() + expect(wrapper.vm.formRules.title[0].required).toBe(true) + }) + + it('应该定义 sortOrder 必填规则', async () => { + wrapper = mount(BannerManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.vm.formRules.sortOrder).toBeDefined() + }) + + // ==================== extractFileId ==================== + + it('extractFileId 应能提取 /api/files/{id}/preview 格式的 ID', async () => { + wrapper = mount(BannerManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.vm.extractFileId('/api/files/123/preview')).toBe('123') + }) + + it('extractFileId 应对纯数字字符串返回该数字', async () => { + wrapper = mount(BannerManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.vm.extractFileId('456')).toBe('456') + }) + + it('extractFileId 应对 null/undefined 返回 null', async () => { + wrapper = mount(BannerManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.vm.extractFileId(null)).toBe(null) + expect(wrapper.vm.extractFileId(undefined)).toBe(null) + }) + + // ==================== beforeUpload ==================== + + it('beforeUpload 应拒绝非图片文件', async () => { + wrapper = mount(BannerManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + const { ElMessage } = await import('element-plus') + + const result = wrapper.vm.beforeUpload(new File([''], 'test.txt', { type: 'text/plain' })) + expect(result).toBe(false) + expect(ElMessage.error).toHaveBeenCalled() + }) + + it('beforeUpload 应拒绝超过 5MB 的图片', async () => { + wrapper = mount(BannerManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + const { ElMessage } = await import('element-plus') + + const largeFile = new File([''], 'large.jpg', { type: 'image/jpeg' }) + Object.defineProperty(largeFile, 'size', { value: 6 * 1024 * 1024 }) + const result = wrapper.vm.beforeUpload(largeFile) + expect(result).toBe(false) + expect(ElMessage.error).toHaveBeenCalled() + }) + + it('beforeUpload 应接受合法图片', async () => { + wrapper = mount(BannerManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + + const validFile = new File([''], 'small.jpg', { type: 'image/jpeg' }) + const result = wrapper.vm.beforeUpload(validFile) + expect(result).toBe(true) + }) + + // ==================== 预览 ==================== + + it('应该暴露 previewImage 方法', async () => { + wrapper = mount(BannerManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(typeof wrapper.vm.previewImage).toBe('function') + }) + + it('初始 previewVisible 应为 false', async () => { + wrapper = mount(BannerManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.vm.previewVisible).toBe(false) + }) +}) diff --git a/gym-manage-web/src/__tests__/components/CoachManagement.test.ts b/gym-manage-web/src/__tests__/components/CoachManagement.test.ts new file mode 100644 index 0000000..6e458ea --- /dev/null +++ b/gym-manage-web/src/__tests__/components/CoachManagement.test.ts @@ -0,0 +1,392 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' +import { mount } from '@vue/test-utils' +import { createRouter, createMemoryHistory } from 'vue-router' +import { createPinia, setActivePinia } from 'pinia' +import CoachManagement from '@/views/system/CoachManagement.vue' + +// ===== Mock Element Plus ===== +vi.mock('element-plus', () => ({ + ElMessage: { success: vi.fn(), error: vi.fn(), warning: vi.fn() }, + ElMessageBox: { confirm: vi.fn() }, +})) + +// ===== Mock @element-plus/icons-vue ===== +vi.mock('@element-plus/icons-vue', () => ({ + Search: { template: '' }, +})) + +// ===== Mock coach.api ===== +vi.mock('@/api/coach.api', () => ({ + coachApi: { + getAll: vi.fn().mockResolvedValue([ + { id: '1', username: 'coach1', nickname: 'Coach One', email: 'coach1@test.com', phone: '13800138001', status: 1, createdAt: '2025-01-01T00:00:00', updatedAt: '2025-01-01T00:00:00' }, + { id: '2', username: 'coach2', nickname: 'Coach Two', email: 'coach2@test.com', phone: '13800138002', status: 0, createdAt: '2025-01-02T00:00:00', updatedAt: '2025-01-02T00:00:00' }, + ]), + getViolationCounts: vi.fn().mockResolvedValue([ + { coach_id: 1, count: 2 }, + { coach_id: 2, count: 0 }, + ]), + create: vi.fn().mockResolvedValue({}), + update: vi.fn().mockResolvedValue({}), + disable: vi.fn().mockResolvedValue({}), + getCourses: vi.fn().mockResolvedValue([]), + getViolations: vi.fn().mockResolvedValue([]), + }, +})) + +// ===== Mock groupCourse.api ===== +vi.mock('@/api/groupCourse.api', () => ({ + groupCourseBookingApi: { + getByCourseId: vi.fn().mockResolvedValue([]), + }, +})) + +// ===== Mock request ===== +vi.mock('@/utils/request', () => { + const mockRequest = { + get: vi.fn().mockResolvedValue({}), + post: vi.fn().mockResolvedValue({}), + put: vi.fn().mockResolvedValue({}), + delete: vi.fn().mockResolvedValue({}), + } + return { default: mockRequest } +}) + +// ===== Mock errorHandler ===== +vi.mock('@/utils/errorHandler', () => ({ + handleApiError: vi.fn(), +})) + +// ===== Mock dateFormat ===== +vi.mock('@/utils/dateFormat', () => ({ + formatDateTime: vi.fn((val: string) => val || '-'), +})) + +// ===== Mock URL.createObjectURL ===== +if (!globalThis.URL.createObjectURL) { + // @ts-ignore + globalThis.URL.createObjectURL = vi.fn(() => `blob:mock-${Math.random()}`) + // @ts-ignore + globalThis.URL.revokeObjectURL = vi.fn() +} + +describe('CoachManagement Component', () => { + let router: any + let wrapper: any + + const defaultStubs = { + 'el-button': { template: '' }, + 'el-card': { template: '
' }, + 'el-input': { template: '', props: ['modelValue'], emits: ['update:modelValue'] }, + 'el-icon': { template: '' }, + 'el-table': { template: '
', props: ['data', 'loading', 'row-class-name'] }, + 'el-table-column': true, + 'el-tag': { template: '', props: ['type', 'effect', 'size', 'round'] }, + 'el-dialog': { template: '
', props: ['modelValue', 'title', 'width'] }, + 'el-form': { template: '
', props: ['model', 'rules', 'label-width', 'ref'] }, + 'el-form-item': { template: '
', props: ['label', 'prop', 'required'] }, + 'el-descriptions': { template: '
', props: ['column', 'border', 'size'] }, + 'el-descriptions-item': { template: '
', props: ['label', 'span'] }, + 'el-empty': true, + } + + beforeEach(() => { + const pinia = createPinia() + setActivePinia(pinia) + + router = createRouter({ + history: createMemoryHistory(), + routes: [{ path: '/', component: { template: '
' } }], + }) + + vi.clearAllMocks() + }) + + afterEach(() => { + if (wrapper) wrapper.unmount() + }) + + // ==================== 组件初始化 ==================== + + it('应该渲染教练管理容器', async () => { + wrapper = mount(CoachManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.find('.coach-management').exists()).toBe(true) + }) + + it('应该渲染搜索区域', async () => { + wrapper = mount(CoachManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.html()).toContain('搜索教练用户名或昵称') + }) + + it('应该渲染新增教练按钮', async () => { + wrapper = mount(CoachManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.html()).toContain('新增教练') + }) + + // ==================== 初始状态 ==================== + + it('初始 loading 状态(onMounted 触发 fetchData 后 loading 启动)', async () => { + wrapper = mount(CoachManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + // onMounted 调用了 fetchData,loading 应为 true(mock API 异步返回) + expect(wrapper.vm.loading).toBe(true) + }) + + it('初始搜索关键词为空', async () => { + wrapper = mount(CoachManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.vm.searchKeyword).toBe('') + }) + + it('初始模态框不可见', async () => { + wrapper = mount(CoachManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.vm.modalVisible).toBe(false) + }) + + // ==================== 方法存在性 ==================== + + it('应该暴露 handleSearch 方法', async () => { + wrapper = mount(CoachManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(typeof wrapper.vm.handleSearch).toBe('function') + }) + + it('应该暴露 handleAdd 方法', async () => { + wrapper = mount(CoachManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(typeof wrapper.vm.handleAdd).toBe('function') + }) + + it('应该暴露 handleEdit 方法', async () => { + wrapper = mount(CoachManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(typeof wrapper.vm.handleEdit).toBe('function') + }) + + it('应该暴露 handleDisable 方法', async () => { + wrapper = mount(CoachManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(typeof wrapper.vm.handleDisable).toBe('function') + }) + + it('应该暴露 handleViewCourses 方法', async () => { + wrapper = mount(CoachManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(typeof wrapper.vm.handleViewCourses).toBe('function') + }) + + it('应该暴露 handleViewDetail 方法', async () => { + wrapper = mount(CoachManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(typeof wrapper.vm.handleViewDetail).toBe('function') + }) + + // ==================== 表单状态绑定 ==================== + + it('点击新增后 modalVisible 应为 true', async () => { + wrapper = mount(CoachManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + wrapper.vm.handleAdd() + await wrapper.vm.$nextTick() + + expect(wrapper.vm.modalVisible).toBe(true) + expect(wrapper.vm.modalTitle).toBe('新增教练') + }) + + it('新增时表单 username 应为空', async () => { + wrapper = mount(CoachManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + wrapper.vm.handleAdd() + await wrapper.vm.$nextTick() + + expect(wrapper.vm.formState.username).toBe('') + }) + + it('新增时表单 password 应为空', async () => { + wrapper = mount(CoachManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + wrapper.vm.handleAdd() + await wrapper.vm.$nextTick() + + expect(wrapper.vm.formState.password).toBe('') + }) + + // ==================== 表单规则 ==================== + + it('应该定义 username 必填规则', async () => { + wrapper = mount(CoachManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.vm.formRules.username).toBeDefined() + expect(wrapper.vm.formRules.username[0].required).toBe(true) + }) + + it('应该定义 password 必填规则', async () => { + wrapper = mount(CoachManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.vm.formRules.password).toBeDefined() + expect(wrapper.vm.formRules.password[0].required).toBe(true) + }) + + it('应该定义 email 必填规则', async () => { + wrapper = mount(CoachManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.vm.formRules.email).toBeDefined() + }) + + // ==================== computed 属性 ==================== + + it('filteredData 应在无搜索词时返回全部数据', async () => { + wrapper = mount(CoachManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + wrapper.vm.dataSource = [ + { id: '1', username: 'coach1', nickname: 'A', email: 'a@t.com', status: 1 }, + { id: '2', username: 'coach2', nickname: 'B', email: 'b@t.com', status: 1 }, + ] + await wrapper.vm.$nextTick() + + expect(wrapper.vm.filteredData.length).toBe(2) + }) + + it('filteredData 应按关键词过滤', async () => { + wrapper = mount(CoachManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + wrapper.vm.dataSource = [ + { id: '1', username: 'coach1', nickname: 'Alpha', email: 'a@t.com', status: 1 }, + { id: '2', username: 'coach2', nickname: 'Beta', email: 'b@t.com', status: 1 }, + ] + wrapper.vm.searchKeyword = 'alpha' + await wrapper.vm.$nextTick() + + expect(wrapper.vm.filteredData.length).toBe(1) + expect(wrapper.vm.filteredData[0].nickname).toBe('Alpha') + }) + + // ==================== 状态映射函数 ==================== + + it('getViolationTypeText 应正确返回违规类型文本', async () => { + wrapper = mount(CoachManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.vm.getViolationTypeText('COACH_LATE')).toBe('教练迟到') + expect(wrapper.vm.getViolationTypeText('COACH_ABSENT')).toBe('教练缺席') + expect(wrapper.vm.getViolationTypeText('NOT_MANUAL_END')).toBe('未手动结课') + expect(wrapper.vm.getViolationTypeText('UNKNOWN')).toBe('UNKNOWN') + }) + + it('getCourseStatusText 应正确返回课程状态文本', async () => { + wrapper = mount(CoachManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.vm.getCourseStatusText(0)).toBe('正常') + expect(wrapper.vm.getCourseStatusText(1)).toBe('已取消') + expect(wrapper.vm.getCourseStatusText(2)).toBe('已结束') + expect(wrapper.vm.getCourseStatusText(99)).toBe('未知') + }) + + // ==================== 违规标签颜色 ==================== + + it('getViolationTag 应在无违规时返回 success', async () => { + wrapper = mount(CoachManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.vm.getViolationTag(999)).toBe('success') + }) + + it('getViolationTag 应在有违规时返回 warning 或 danger', async () => { + wrapper = mount(CoachManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + wrapper.vm.violationCounts[1] = 1 + expect(wrapper.vm.getViolationTag(1)).toBe('warning') + + wrapper.vm.violationCounts[1] = 5 + expect(wrapper.vm.getViolationTag(1)).toBe('danger') + }) + + // ==================== 取消弹窗 ==================== + + it('handleModalCancel 应关闭弹窗', async () => { + wrapper = mount(CoachManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + wrapper.vm.modalVisible = true + wrapper.vm.handleModalCancel() + await wrapper.vm.$nextTick() + + expect(wrapper.vm.modalVisible).toBe(false) + }) +}) diff --git a/gym-manage-web/src/__tests__/components/CourseLabelManagement.test.ts b/gym-manage-web/src/__tests__/components/CourseLabelManagement.test.ts new file mode 100644 index 0000000..10ba1fb --- /dev/null +++ b/gym-manage-web/src/__tests__/components/CourseLabelManagement.test.ts @@ -0,0 +1,247 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' +import { mount } from '@vue/test-utils' +import { createRouter, createMemoryHistory } from 'vue-router' +import { createPinia, setActivePinia } from 'pinia' +import CourseLabelManagement from '@/views/groupcourse/CourseLabelManagement.vue' + +// ===== Mock Element Plus ===== +vi.mock('element-plus', () => ({ + ElMessage: { success: vi.fn(), error: vi.fn(), warning: vi.fn() }, + ElMessageBox: { confirm: vi.fn() }, +})) + +// ===== Mock @element-plus/icons-vue ===== +vi.mock('@element-plus/icons-vue', () => ({ + Search: { template: '' }, +})) + +// ===== Mock groupCourse.api ===== +vi.mock('@/api/groupCourse.api', () => ({ + courseLabelApi: { + getPage: vi.fn().mockResolvedValue({ + content: [ + { id: 1, labelName: '热门', color: '#FF0000', description: '热门标签' }, + { id: 2, labelName: '新手', color: '#00FF00', description: '新手友好' }, + ], + totalElements: 2, + }), + create: vi.fn().mockResolvedValue({}), + update: vi.fn().mockResolvedValue({}), + delete: vi.fn().mockResolvedValue({}), + }, +})) + +describe('CourseLabelManagement Component', () => { + let router: any + let wrapper: any + + const defaultStubs = { + 'el-button': { template: '', props: ['type', 'size', 'link'] }, + 'el-card': { template: '
' }, + 'el-input': { template: '', props: ['modelValue'], emits: ['update:modelValue'] }, + 'el-icon': { template: '' }, + 'el-table': { template: '
', props: ['data', 'loading'] }, + 'el-table-column': true, + 'el-tag': { template: '', props: ['type', 'effect', 'size', 'color'] }, + 'el-pagination': true, + 'el-dialog': { template: '
', props: ['modelValue', 'title', 'width'] }, + 'el-form': { template: '
', props: ['model', 'rules', 'label-width', 'ref'] }, + 'el-form-item': { template: '
', props: ['label', 'prop'] }, + 'el-color-picker': true, + } + + beforeEach(() => { + const pinia = createPinia() + setActivePinia(pinia) + + router = createRouter({ + history: createMemoryHistory(), + routes: [{ path: '/', component: { template: '
' } }], + }) + + vi.clearAllMocks() + }) + + afterEach(() => { + if (wrapper) wrapper.unmount() + }) + + // ==================== 组件初始化 ==================== + + it('应该渲染标签管理容器', async () => { + wrapper = mount(CourseLabelManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.find('.course-label-management').exists()).toBe(true) + }) + + it('应该渲染搜索区域', async () => { + wrapper = mount(CourseLabelManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.html()).toContain('搜索标签名称') + }) + + it('应该渲染新增标签按钮', async () => { + wrapper = mount(CourseLabelManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.html()).toContain('新增标签') + }) + + // ==================== 初始状态 ==================== + + it('初始 loading 状态为 false', async () => { + wrapper = mount(CourseLabelManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.vm.loading).toBe(false) + }) + + it('初始搜索关键词为空', async () => { + wrapper = mount(CourseLabelManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.vm.searchKeyword).toBe('') + }) + + it('初始 modalVisible 为 false', async () => { + wrapper = mount(CourseLabelManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.vm.modalVisible).toBe(false) + }) + + it('初始 currentPage 为 1', async () => { + wrapper = mount(CourseLabelManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.vm.currentPage).toBe(1) + }) + + it('初始 pageSize 为 10', async () => { + wrapper = mount(CourseLabelManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.vm.pageSize).toBe(10) + }) + + // ==================== 方法存在性 ==================== + + it('应该暴露 handleSearch 方法', async () => { + wrapper = mount(CourseLabelManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(typeof wrapper.vm.handleSearch).toBe('function') + }) + + it('应该暴露 handleAdd 方法', async () => { + wrapper = mount(CourseLabelManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(typeof wrapper.vm.handleAdd).toBe('function') + }) + + it('应该暴露 handleEdit 方法', async () => { + wrapper = mount(CourseLabelManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(typeof wrapper.vm.handleEdit).toBe('function') + }) + + it('应该暴露 handleDelete 方法', async () => { + wrapper = mount(CourseLabelManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(typeof wrapper.vm.handleDelete).toBe('function') + }) + + // ==================== 新增弹窗 ==================== + + it('handleAdd 应打开新增弹窗并重置表单', async () => { + wrapper = mount(CourseLabelManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + wrapper.vm.handleAdd() + await wrapper.vm.$nextTick() + + expect(wrapper.vm.modalVisible).toBe(true) + expect(wrapper.vm.modalTitle).toBe('新增标签') + expect(wrapper.vm.formState.labelName).toBe('') + }) + + // ==================== 表单规则 ==================== + + it('应定义 labelName 必填规则', async () => { + wrapper = mount(CourseLabelManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.vm.formRules.labelName).toBeDefined() + expect(wrapper.vm.formRules.labelName[0].required).toBe(true) + }) + + it('应定义 color 必填规则', async () => { + wrapper = mount(CourseLabelManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + expect(wrapper.vm.formRules.color).toBeDefined() + }) + + // ==================== 分页操作 ==================== + + it('onSizeChange 应重置当前页为 1', async () => { + wrapper = mount(CourseLabelManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + wrapper.vm.currentPage = 3 + wrapper.vm.onSizeChange() + await wrapper.vm.$nextTick() + + expect(wrapper.vm.currentPage).toBe(1) + }) + + it('handleSearch 应重置当前页为 1', async () => { + wrapper = mount(CourseLabelManagement, { + global: { plugins: [router], stubs: defaultStubs }, + }) + + await wrapper.vm.$nextTick() + wrapper.vm.currentPage = 3 + wrapper.vm.handleSearch() + await wrapper.vm.$nextTick() + + expect(wrapper.vm.currentPage).toBe(1) + }) +}) diff --git a/gym-manage-web/src/__tests__/components/CourseRecommendManagement.test.ts b/gym-manage-web/src/__tests__/components/CourseRecommendManagement.test.ts new file mode 100644 index 0000000..b7a89f5 --- /dev/null +++ b/gym-manage-web/src/__tests__/components/CourseRecommendManagement.test.ts @@ -0,0 +1,251 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' +import { mount } from '@vue/test-utils' +import { createRouter, createMemoryHistory } from 'vue-router' +import { createPinia, setActivePinia } from 'pinia' +import CourseRecommendManagement from '@/views/groupcourse/CourseRecommendManagement.vue' + +// ===== Mock Element Plus ===== +vi.mock('element-plus', () => ({ + ElMessage: { success: vi.fn(), error: vi.fn(), warning: vi.fn() }, + ElMessageBox: { confirm: vi.fn() }, +})) + +// ===== Mock @element-plus/icons-vue ===== +vi.mock('@element-plus/icons-vue', () => ({ + Search: { template: '' }, +})) + +// ===== Mock groupCourse.api ===== +vi.mock('@/api/groupCourse.api', () => ({ + groupCourseRecommendApi: { + getAll: vi.fn().mockResolvedValue([ + { id: 1, courseId: 1, recommendTitle: '推荐课程1', recommendContent: '内容1', recommendReason: '理由1', priority: 3, isActive: true, createdAt: '2025-01-01T00:00:00' }, + { id: 2, courseId: 2, recommendTitle: '推荐课程2', recommendContent: '内容2', recommendReason: '理由2', priority: 5, isActive: false, createdAt: '2025-01-02T00:00:00' }, + ]), + create: vi.fn().mockResolvedValue({}), + update: vi.fn().mockResolvedValue({}), + delete: vi.fn().mockResolvedValue({}), + toggleActive: vi.fn().mockResolvedValue({}), + }, + groupCourseApi: { + getAll: vi.fn().mockResolvedValue([ + { id: 1, courseName: '团课A' }, + { id: 2, courseName: '团课B' }, + ]), + }, +})) + +// ===== Mock dateFormat ===== +vi.mock('@/utils/dateFormat', () => ({ + formatDateTime: vi.fn((val: string) => val || '-'), +})) + +describe('CourseRecommendManagement Component', () => { + let router: any + let wrapper: any + + const defaultStubs = { + 'el-button': { template: '', props: ['type', 'size', 'link'] }, + 'el-card': { template: '
' }, + 'el-input': { template: '', props: ['modelValue'], emits: ['update:modelValue'] }, + 'el-icon': { template: '' }, + 'el-table': { template: '
', props: ['data', 'loading'] }, + 'el-table-column': true, + 'el-tag': { template: '', props: ['type', 'effect', 'size'] }, + 'el-dialog': { template: '
', props: ['modelValue', 'title', 'width'] }, + 'el-form': { template: '
', props: ['model', 'rules', 'label-width', 'ref'] }, + 'el-form-item': { template: '
', props: ['label', 'prop'] }, + 'el-select': { template: '', props: ['modelValue'], emits: ['update:modelValue'] }, + 'el-option': { template: '