|
|
@@ -0,0 +1,389 @@
|
|
|
+package com.example.resourceplatform.service.impl;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
+import com.example.resourceplatform.common.BusinessException;
|
|
|
+import com.example.resourceplatform.common.ResourceStatus;
|
|
|
+import com.example.resourceplatform.dto.ResourceDTO;
|
|
|
+import com.example.resourceplatform.dto.ResourceVO;
|
|
|
+import com.example.resourceplatform.entity.ProjectResource;
|
|
|
+import com.example.resourceplatform.entity.Resource;
|
|
|
+import com.example.resourceplatform.entity.ResourceType;
|
|
|
+import com.example.resourceplatform.entity.Provider;
|
|
|
+import com.example.resourceplatform.entity.Project;
|
|
|
+import com.example.resourceplatform.mapper.*;
|
|
|
+import com.example.resourceplatform.service.OperationLogService;
|
|
|
+import com.example.resourceplatform.service.SysUserService;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.DisplayName;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.junit.jupiter.api.extension.ExtendWith;
|
|
|
+import org.mockito.InjectMocks;
|
|
|
+import org.mockito.Mock;
|
|
|
+import org.mockito.junit.jupiter.MockitoExtension;
|
|
|
+import org.springframework.test.util.ReflectionTestUtils;
|
|
|
+
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.time.LocalDate;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+import static org.assertj.core.api.Assertions.*;
|
|
|
+import static org.junit.jupiter.api.Assertions.*;
|
|
|
+import static org.mockito.ArgumentMatchers.*;
|
|
|
+import static org.mockito.Mockito.*;
|
|
|
+
|
|
|
+@ExtendWith(MockitoExtension.class)
|
|
|
+class ResourceServiceImplTest {
|
|
|
+
|
|
|
+ @Mock
|
|
|
+ private ResourceMapper resourceMapper;
|
|
|
+ @Mock
|
|
|
+ private ResourceTypeMapper resourceTypeMapper;
|
|
|
+ @Mock
|
|
|
+ private ProviderMapper providerMapper;
|
|
|
+ @Mock
|
|
|
+ private ProjectMapper projectMapper;
|
|
|
+ @Mock
|
|
|
+ private ProjectResourceMapper projectResourceMapper;
|
|
|
+ @Mock
|
|
|
+ private ResourceRenewalMapper renewalMapper;
|
|
|
+ @Mock
|
|
|
+ private SysUserService sysUserService;
|
|
|
+ @Mock
|
|
|
+ private OperationLogService operationLogService;
|
|
|
+
|
|
|
+ @InjectMocks
|
|
|
+ private ResourceServiceImpl resourceService;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp() {
|
|
|
+ // 设置 @Value 注入的配置属性
|
|
|
+ ReflectionTestUtils.setField(resourceService, "remindDaysFar", 30);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ========== getDetail ==========
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("getDetail: 资源存在时返回VO")
|
|
|
+ void getDetail_resourceExists_returnsVO() {
|
|
|
+ Resource resource = buildResource(1L, "test-resource", 1L, 2L, 10L);
|
|
|
+ when(resourceMapper.selectById(1L)).thenReturn(resource);
|
|
|
+ when(resourceTypeMapper.selectBatchIds(anyCollection()))
|
|
|
+ .thenReturn(List.of(buildResourceType(1L, "云服务器")));
|
|
|
+ when(providerMapper.selectBatchIds(anyCollection()))
|
|
|
+ .thenReturn(List.of(buildProvider(2L, "阿里云")));
|
|
|
+ when(sysUserService.getUserNameMap(anyList()))
|
|
|
+ .thenReturn(Map.of(10L, "张三"));
|
|
|
+ when(projectResourceMapper.selectList(any(LambdaQueryWrapper.class)))
|
|
|
+ .thenReturn(Collections.emptyList());
|
|
|
+
|
|
|
+ ResourceVO vo = resourceService.getDetail(1L);
|
|
|
+
|
|
|
+ assertThat(vo).isNotNull();
|
|
|
+ assertThat(vo.getId()).isEqualTo(1L);
|
|
|
+ assertThat(vo.getName()).isEqualTo("test-resource");
|
|
|
+ assertThat(vo.getTypeName()).isEqualTo("云服务器");
|
|
|
+ assertThat(vo.getProviderName()).isEqualTo("阿里云");
|
|
|
+ assertThat(vo.getOwnerName()).isEqualTo("张三");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("getDetail: 资源不存在时抛出BusinessException")
|
|
|
+ void getDetail_resourceNotFound_throwsException() {
|
|
|
+ when(resourceMapper.selectById(999L)).thenReturn(null);
|
|
|
+
|
|
|
+ BusinessException ex = assertThrows(BusinessException.class,
|
|
|
+ () -> resourceService.getDetail(999L));
|
|
|
+ assertThat(ex.getMessage()).isEqualTo("资源不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ========== listByIds ==========
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("listByIds: 空列表返回空结果")
|
|
|
+ void listByIds_emptyList_returnsEmpty() {
|
|
|
+ List<ResourceVO> result = resourceService.listByIds(Collections.emptyList());
|
|
|
+ assertThat(result).isEmpty();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("listByIds: null列表返回空结果")
|
|
|
+ void listByIds_nullList_returnsEmpty() {
|
|
|
+ List<ResourceVO> result = resourceService.listByIds(null);
|
|
|
+ assertThat(result).isEmpty();
|
|
|
+ }
|
|
|
+
|
|
|
+ // ========== saveOrUpdate ==========
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("saveOrUpdate: 到期日期早于开通日期时抛出异常")
|
|
|
+ void saveOrUpdate_expireBeforeStart_throwsException() {
|
|
|
+ ResourceDTO dto = new ResourceDTO();
|
|
|
+ dto.setName("test");
|
|
|
+ dto.setTypeId(1L);
|
|
|
+ dto.setOwnerId(10L);
|
|
|
+ dto.setStartDate(LocalDate.of(2026, 8, 15));
|
|
|
+ dto.setExpireDate(LocalDate.of(2026, 8, 10)); // 到期日期早于开通日期
|
|
|
+
|
|
|
+ BusinessException ex = assertThrows(BusinessException.class,
|
|
|
+ () -> resourceService.saveOrUpdate(dto));
|
|
|
+ assertThat(ex.getMessage()).isEqualTo("到期日期不能早于开通日期");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("saveOrUpdate: 新增资源时设置状态为ACTIVE并调用insert")
|
|
|
+ void saveOrUpdate_newResource_insertsWithActiveStatus() {
|
|
|
+ ResourceDTO dto = new ResourceDTO();
|
|
|
+ dto.setName("new-resource");
|
|
|
+ dto.setTypeId(1L);
|
|
|
+ dto.setProviderId(2L);
|
|
|
+ dto.setOwnerId(10L);
|
|
|
+ dto.setCost(new BigDecimal("100.00"));
|
|
|
+ dto.setStartDate(LocalDate.of(2026, 1, 1));
|
|
|
+ dto.setExpireDate(LocalDate.of(2026, 12, 31));
|
|
|
+ // id为null -> 新增
|
|
|
+
|
|
|
+ when(resourceMapper.insert(any(Resource.class))).thenReturn(1);
|
|
|
+ when(projectResourceMapper.delete(any(LambdaQueryWrapper.class))).thenReturn(0);
|
|
|
+
|
|
|
+ resourceService.saveOrUpdate(dto);
|
|
|
+
|
|
|
+ verify(resourceMapper).insert(argThat(r -> {
|
|
|
+ assertThat(r.getName()).isEqualTo("new-resource");
|
|
|
+ assertThat(r.getStatus()).isEqualTo(ResourceStatus.ACTIVE.getCode());
|
|
|
+ assertThat(r.getCost()).isEqualByComparingTo(new BigDecimal("100.00"));
|
|
|
+ return true;
|
|
|
+ }));
|
|
|
+ verify(operationLogService).record(eq("资源"), eq("新增"), any(), contains("new-resource"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("saveOrUpdate: 编辑已有资源时调用updateById")
|
|
|
+ void saveOrUpdate_existingResource_callsUpdateById() {
|
|
|
+ Resource existing = buildResource(1L, "old-name", 1L, 2L, 10L);
|
|
|
+ when(resourceMapper.selectById(1L)).thenReturn(existing);
|
|
|
+ when(projectResourceMapper.delete(any(LambdaQueryWrapper.class))).thenReturn(0);
|
|
|
+
|
|
|
+ ResourceDTO dto = new ResourceDTO();
|
|
|
+ dto.setId(1L);
|
|
|
+ dto.setName("new-name");
|
|
|
+ dto.setTypeId(1L);
|
|
|
+ dto.setOwnerId(10L);
|
|
|
+
|
|
|
+ resourceService.saveOrUpdate(dto);
|
|
|
+
|
|
|
+ verify(resourceMapper).updateById(argThat(r -> {
|
|
|
+ assertThat(r.getName()).isEqualTo("new-name");
|
|
|
+ return true;
|
|
|
+ }));
|
|
|
+ verify(operationLogService).record(eq("资源"), eq("编辑"), eq(1L), contains("new-name"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("saveOrUpdate: 编辑不存在的资源抛出异常")
|
|
|
+ void saveOrUpdate_editNonExistent_throwsException() {
|
|
|
+ when(resourceMapper.selectById(999L)).thenReturn(null);
|
|
|
+
|
|
|
+ ResourceDTO dto = new ResourceDTO();
|
|
|
+ dto.setId(999L);
|
|
|
+ dto.setName("test");
|
|
|
+ dto.setTypeId(1L);
|
|
|
+ dto.setOwnerId(10L);
|
|
|
+
|
|
|
+ assertThrows(BusinessException.class, () -> resourceService.saveOrUpdate(dto));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("saveOrUpdate: 新增资源带项目关联,创建ProjectResource记录")
|
|
|
+ void saveOrUpdate_withProjectIds_createsProjectResources() {
|
|
|
+ ResourceDTO dto = new ResourceDTO();
|
|
|
+ dto.setName("test");
|
|
|
+ dto.setTypeId(1L);
|
|
|
+ dto.setOwnerId(10L);
|
|
|
+ dto.setProjectIds(List.of(100L, 200L));
|
|
|
+
|
|
|
+ when(resourceMapper.insert(any(Resource.class))).thenAnswer(inv -> {
|
|
|
+ Resource r = inv.getArgument(0);
|
|
|
+ r.setId(1L); // 模拟自增id
|
|
|
+ return 1;
|
|
|
+ });
|
|
|
+ when(projectResourceMapper.delete(any(LambdaQueryWrapper.class))).thenReturn(0);
|
|
|
+
|
|
|
+ resourceService.saveOrUpdate(dto);
|
|
|
+
|
|
|
+ // 应该为两个项目各创建一条关联记录
|
|
|
+ verify(projectResourceMapper, times(2)).insert(any(ProjectResource.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("saveOrUpdate: cost为null时默认为ZERO")
|
|
|
+ void saveOrUpdate_nullCost_defaultsToZero() {
|
|
|
+ ResourceDTO dto = new ResourceDTO();
|
|
|
+ dto.setName("test");
|
|
|
+ dto.setTypeId(1L);
|
|
|
+ dto.setOwnerId(10L);
|
|
|
+ dto.setCost(null);
|
|
|
+
|
|
|
+ when(resourceMapper.insert(any(Resource.class))).thenReturn(1);
|
|
|
+ when(projectResourceMapper.delete(any(LambdaQueryWrapper.class))).thenReturn(0);
|
|
|
+
|
|
|
+ resourceService.saveOrUpdate(dto);
|
|
|
+
|
|
|
+ verify(resourceMapper).insert(argThat(r -> {
|
|
|
+ assertThat(r.getCost()).isEqualByComparingTo(BigDecimal.ZERO);
|
|
|
+ return true;
|
|
|
+ }));
|
|
|
+ }
|
|
|
+
|
|
|
+ // ========== delete ==========
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("delete: 删除资源并清理项目关联")
|
|
|
+ void delete_deletesResourceAndRelations() {
|
|
|
+ when(resourceMapper.deleteById(1L)).thenReturn(1);
|
|
|
+ when(projectResourceMapper.delete(any(LambdaQueryWrapper.class))).thenReturn(2);
|
|
|
+
|
|
|
+ resourceService.delete(1L);
|
|
|
+
|
|
|
+ verify(resourceMapper).deleteById(1L);
|
|
|
+ verify(projectResourceMapper).delete(any(LambdaQueryWrapper.class));
|
|
|
+ verify(operationLogService).record("资源", "删除", 1L, "资源id: 1");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ========== refreshExpireStatus ==========
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("refreshExpireStatus: 已过期资源状态更新为EXPIRED")
|
|
|
+ void refreshExpireStatus_expiredResource_setsExpiredStatus() {
|
|
|
+ Resource r = buildResource(1L, "expired", 1L, 2L, 10L);
|
|
|
+ r.setExpireDate(LocalDate.now().minusDays(5));
|
|
|
+ r.setStatus(ResourceStatus.ACTIVE.getCode());
|
|
|
+
|
|
|
+ when(resourceMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(r));
|
|
|
+
|
|
|
+ resourceService.refreshExpireStatus();
|
|
|
+
|
|
|
+ verify(resourceMapper).updateById(argThat(res -> {
|
|
|
+ assertThat(res.getStatus()).isEqualTo(ResourceStatus.EXPIRED.getCode());
|
|
|
+ return true;
|
|
|
+ }));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("refreshExpireStatus: 即将到期资源状态更新为EXPIRING")
|
|
|
+ void refreshExpireStatus_expiringSoon_setsExpiringStatus() {
|
|
|
+ Resource r = buildResource(1L, "expiring", 1L, 2L, 10L);
|
|
|
+ r.setExpireDate(LocalDate.now().plusDays(10)); // 10天后到期,在30天提醒范围内
|
|
|
+ r.setStatus(ResourceStatus.ACTIVE.getCode());
|
|
|
+
|
|
|
+ when(resourceMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(r));
|
|
|
+
|
|
|
+ resourceService.refreshExpireStatus();
|
|
|
+
|
|
|
+ verify(resourceMapper).updateById(argThat(res -> {
|
|
|
+ assertThat(res.getStatus()).isEqualTo(ResourceStatus.EXPIRING.getCode());
|
|
|
+ return true;
|
|
|
+ }));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("refreshExpireStatus: 正常资源状态保持ACTIVE")
|
|
|
+ void refreshExpireStatus_activeResource_staysActive() {
|
|
|
+ Resource r = buildResource(1L, "active", 1L, 2L, 10L);
|
|
|
+ r.setExpireDate(LocalDate.now().plusDays(60)); // 60天后到期,超出30天提醒范围
|
|
|
+ r.setStatus(ResourceStatus.ACTIVE.getCode());
|
|
|
+
|
|
|
+ when(resourceMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(r));
|
|
|
+
|
|
|
+ resourceService.refreshExpireStatus();
|
|
|
+
|
|
|
+ // 状态未变,不应调用updateById
|
|
|
+ verify(resourceMapper, never()).updateById(any(Resource.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("refreshExpireStatus: expireDate为null时跳过该资源")
|
|
|
+ void refreshExpireStatus_nullExpireDate_skips() {
|
|
|
+ Resource r = buildResource(1L, "no-expire", 1L, 2L, 10L);
|
|
|
+ r.setExpireDate(null);
|
|
|
+ r.setStatus(ResourceStatus.ACTIVE.getCode());
|
|
|
+
|
|
|
+ when(resourceMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(r));
|
|
|
+
|
|
|
+ resourceService.refreshExpireStatus();
|
|
|
+
|
|
|
+ verify(resourceMapper, never()).updateById(any(Resource.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ // ========== costStat ==========
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("costStatByProject: range为null时不过滤时间")
|
|
|
+ void costStatByProject_nullRange_noDateFilter() {
|
|
|
+ when(resourceMapper.costStatByProject(isNull(), isNull()))
|
|
|
+ .thenReturn(Collections.emptyList());
|
|
|
+
|
|
|
+ var result = resourceService.costStatByProject(null);
|
|
|
+
|
|
|
+ assertThat(result).isEmpty();
|
|
|
+ verify(resourceMapper).costStatByProject(null, null);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("costStatByProject: range=month时传入本月时间范围")
|
|
|
+ void costStatByProject_monthRange_passesMonthDates() {
|
|
|
+ when(resourceMapper.costStatByProject(any(), any()))
|
|
|
+ .thenReturn(Collections.emptyList());
|
|
|
+
|
|
|
+ resourceService.costStatByProject("month");
|
|
|
+
|
|
|
+ verify(resourceMapper).costStatByProject(
|
|
|
+ argThat(d -> d != null && d.equals(LocalDate.now().withDayOfMonth(1))),
|
|
|
+ argThat(d -> d != null));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("costStatByType: range=year时传入本年时间范围")
|
|
|
+ void costStatByType_yearRange_passesYearDates() {
|
|
|
+ when(resourceMapper.costStatByType(any(), any()))
|
|
|
+ .thenReturn(Collections.emptyList());
|
|
|
+
|
|
|
+ resourceService.costStatByType("year");
|
|
|
+
|
|
|
+ verify(resourceMapper).costStatByType(
|
|
|
+ argThat(d -> d != null && d.getMonthValue() == 1 && d.getDayOfMonth() == 1),
|
|
|
+ argThat(d -> d != null));
|
|
|
+ }
|
|
|
+
|
|
|
+ // ========== 辅助方法 ==========
|
|
|
+
|
|
|
+ private Resource buildResource(Long id, String name, Long typeId, Long providerId, Long ownerId) {
|
|
|
+ Resource r = new Resource();
|
|
|
+ r.setId(id);
|
|
|
+ r.setName(name);
|
|
|
+ r.setTypeId(typeId);
|
|
|
+ r.setProviderId(providerId);
|
|
|
+ r.setOwnerId(ownerId);
|
|
|
+ r.setCost(new BigDecimal("500.00"));
|
|
|
+ r.setStatus(ResourceStatus.ACTIVE.getCode());
|
|
|
+ r.setStartDate(LocalDate.of(2026, 1, 1));
|
|
|
+ r.setExpireDate(LocalDate.of(2026, 12, 31));
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+
|
|
|
+ private ResourceType buildResourceType(Long id, String name) {
|
|
|
+ ResourceType rt = new ResourceType();
|
|
|
+ rt.setId(id);
|
|
|
+ rt.setName(name);
|
|
|
+ return rt;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Provider buildProvider(Long id, String name) {
|
|
|
+ Provider p = new Provider();
|
|
|
+ p.setId(id);
|
|
|
+ p.setName(name);
|
|
|
+ return p;
|
|
|
+ }
|
|
|
+}
|