test(G4): 巡店删除 ResolveService 补 13 个契约测试(该模块此前零测试文件)
覆盖:用户校验、索引未命中/同名冲突拒绝、重复添加幂等、他人记录不可删(同文案防探测)、 匹配结果去重保序、count 空值兜底。
This commit is contained in:
+238
@@ -0,0 +1,238 @@
|
|||||||
|
package com.nanri.aiimage.modules.patroldelete.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||||
|
import com.nanri.aiimage.common.exception.BusinessException;
|
||||||
|
import com.nanri.aiimage.common.model.vo.ProductRiskCandidateVo;
|
||||||
|
import com.nanri.aiimage.common.model.vo.ProductRiskMatchShopsVo;
|
||||||
|
import com.nanri.aiimage.modules.patroldelete.mapper.PatrolDeleteConditionMapper;
|
||||||
|
import com.nanri.aiimage.modules.patroldelete.mapper.PatrolDeleteShopCandidateMapper;
|
||||||
|
import com.nanri.aiimage.modules.patroldelete.model.entity.PatrolDeleteShopCandidateEntity;
|
||||||
|
import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskCandidateAddRequest;
|
||||||
|
import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskMatchShopsRequest;
|
||||||
|
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoShopMatchResultVo;
|
||||||
|
import com.nanri.aiimage.modules.ziniao.service.ZiniaoShopIndexService;
|
||||||
|
import com.nanri.aiimage.modules.ziniao.service.ZiniaoShopSwitchService;
|
||||||
|
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.transaction.PlatformTransactionManager;
|
||||||
|
import org.springframework.transaction.TransactionStatus;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 巡店删除的备选店铺解析(2026-09 审查 G4:该模块此前零测试文件,且出过
|
||||||
|
* "逐国上报失败仍判成功"的事故,先从有明确契约的 ResolveService 补起)。
|
||||||
|
*/
|
||||||
|
class PatrolDeleteResolveServiceTest {
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void initializeMybatisMetadata() {
|
||||||
|
TableInfoHelper.initTableInfo(
|
||||||
|
new MapperBuilderAssistant(new MybatisConfiguration(), ""),
|
||||||
|
PatrolDeleteShopCandidateEntity.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final PatrolDeleteShopCandidateMapper candidateMapper = mock(PatrolDeleteShopCandidateMapper.class);
|
||||||
|
private final PatrolDeleteConditionMapper conditionMapper = mock(PatrolDeleteConditionMapper.class);
|
||||||
|
private final ZiniaoShopSwitchService shopSwitchService = mock(ZiniaoShopSwitchService.class);
|
||||||
|
private final PlatformTransactionManager transactionManager = mock(PlatformTransactionManager.class);
|
||||||
|
|
||||||
|
private PatrolDeleteResolveService service;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
service = new PatrolDeleteResolveService(candidateMapper, conditionMapper, shopSwitchService, transactionManager);
|
||||||
|
when(transactionManager.getTransaction(any())).thenReturn(mock(TransactionStatus.class));
|
||||||
|
when(shopSwitchService.normalizeShopName(anyString())).thenAnswer(invocation ->
|
||||||
|
((String) invocation.getArgument(0)).trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProductRiskCandidateAddRequest addRequest(Long userId, String shopName) {
|
||||||
|
ProductRiskCandidateAddRequest request = new ProductRiskCandidateAddRequest();
|
||||||
|
request.setUserId(userId);
|
||||||
|
request.setShopName(shopName);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ZiniaoShopMatchResultVo matched(String shopName) {
|
||||||
|
ZiniaoShopMatchResultVo vo = new ZiniaoShopMatchResultVo();
|
||||||
|
vo.setMatched(true);
|
||||||
|
vo.setShopName(shopName);
|
||||||
|
vo.setMatchStatus(ZiniaoShopIndexService.MATCH_STATUS_MATCHED);
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listCandidatesRejectsInvalidUser() {
|
||||||
|
assertThrows(BusinessException.class, () -> service.listCandidates(null));
|
||||||
|
assertThrows(BusinessException.class, () -> service.listCandidates(0L));
|
||||||
|
verify(candidateMapper, never()).selectList(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
void listCandidatesMapsRowsToViews() {
|
||||||
|
PatrolDeleteShopCandidateEntity row = new PatrolDeleteShopCandidateEntity();
|
||||||
|
row.setId(7L);
|
||||||
|
row.setUserId(23L);
|
||||||
|
row.setShopName("测试店铺");
|
||||||
|
row.setCreatedAt(LocalDateTime.now());
|
||||||
|
when(candidateMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(row));
|
||||||
|
|
||||||
|
List<ProductRiskCandidateVo> views = service.listCandidates(23L);
|
||||||
|
|
||||||
|
assertEquals(1, views.size());
|
||||||
|
assertEquals(7L, views.getFirst().getId());
|
||||||
|
assertEquals("测试店铺", views.getFirst().getShopName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addCandidateRejectsBlankShopName() {
|
||||||
|
when(shopSwitchService.normalizeShopName(" ")).thenReturn("");
|
||||||
|
|
||||||
|
assertThrows(BusinessException.class, () -> service.addCandidate(addRequest(23L, " ")));
|
||||||
|
verify(candidateMapper, never()).insert(any(PatrolDeleteShopCandidateEntity.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addCandidateRejectsWhenIndexMisses() {
|
||||||
|
ZiniaoShopMatchResultVo miss = new ZiniaoShopMatchResultVo();
|
||||||
|
miss.setMatched(false);
|
||||||
|
miss.setMatchMessage("店铺索引未命中,无法加入备选");
|
||||||
|
when(shopSwitchService.findIndexedStoreByName("未收录店铺", false)).thenReturn(miss);
|
||||||
|
|
||||||
|
BusinessException exception = assertThrows(BusinessException.class,
|
||||||
|
() -> service.addCandidate(addRequest(23L, "未收录店铺")));
|
||||||
|
|
||||||
|
assertEquals("店铺索引未命中,无法加入备选", exception.getMessage());
|
||||||
|
verify(candidateMapper, never()).insert(any(PatrolDeleteShopCandidateEntity.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addCandidateRejectsConflictStatus() {
|
||||||
|
ZiniaoShopMatchResultVo conflict = matched("同名店铺");
|
||||||
|
conflict.setMatchStatus(ZiniaoShopIndexService.MATCH_STATUS_CONFLICT);
|
||||||
|
conflict.setMatchMessage("存在多个同名店铺,请人工确认");
|
||||||
|
when(shopSwitchService.findIndexedStoreByName("同名店铺", false)).thenReturn(conflict);
|
||||||
|
|
||||||
|
BusinessException exception = assertThrows(BusinessException.class,
|
||||||
|
() -> service.addCandidate(addRequest(23L, "同名店铺")));
|
||||||
|
|
||||||
|
assertEquals("存在多个同名店铺,请人工确认", exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
void addCandidateIsIdempotentForExistingShop() {
|
||||||
|
when(shopSwitchService.findIndexedStoreByName("已存在店铺", false)).thenReturn(matched("已存在店铺"));
|
||||||
|
PatrolDeleteShopCandidateEntity existing = new PatrolDeleteShopCandidateEntity();
|
||||||
|
existing.setId(31L);
|
||||||
|
existing.setUserId(23L);
|
||||||
|
existing.setShopName("已存在店铺");
|
||||||
|
existing.setCreatedAt(LocalDateTime.now());
|
||||||
|
when(candidateMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(existing);
|
||||||
|
|
||||||
|
ProductRiskCandidateVo vo = service.addCandidate(addRequest(23L, " 已存在店铺 "));
|
||||||
|
|
||||||
|
assertEquals(31L, vo.getId(), "重复添加返回既有记录");
|
||||||
|
verify(candidateMapper, never()).insert(any(PatrolDeleteShopCandidateEntity.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
void addCandidateInsertsWhenIndexMatched() {
|
||||||
|
when(shopSwitchService.findIndexedStoreByName("新店铺", false)).thenReturn(matched("新店铺"));
|
||||||
|
when(candidateMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(null);
|
||||||
|
when(candidateMapper.insert(any(PatrolDeleteShopCandidateEntity.class))).thenAnswer(invocation -> {
|
||||||
|
PatrolDeleteShopCandidateEntity entity = invocation.getArgument(0);
|
||||||
|
entity.setId(88L);
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
ProductRiskCandidateVo vo = service.addCandidate(addRequest(23L, "新店铺"));
|
||||||
|
|
||||||
|
assertEquals(88L, vo.getId());
|
||||||
|
assertEquals("新店铺", vo.getShopName());
|
||||||
|
verify(candidateMapper).insert(any(PatrolDeleteShopCandidateEntity.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deleteCandidateRejectsAnotherUsersRecord() {
|
||||||
|
PatrolDeleteShopCandidateEntity other = new PatrolDeleteShopCandidateEntity();
|
||||||
|
other.setId(5L);
|
||||||
|
other.setUserId(99L);
|
||||||
|
when(candidateMapper.selectById(5L)).thenReturn(other);
|
||||||
|
|
||||||
|
BusinessException exception = assertThrows(BusinessException.class,
|
||||||
|
() -> service.deleteCandidate(23L, 5L));
|
||||||
|
|
||||||
|
assertEquals("记录不存在", exception.getMessage(), "他人记录与不存在同文案,避免探测");
|
||||||
|
verify(candidateMapper, never()).deleteById(5L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deleteCandidateRemovesOwnRecord() {
|
||||||
|
PatrolDeleteShopCandidateEntity own = new PatrolDeleteShopCandidateEntity();
|
||||||
|
own.setId(5L);
|
||||||
|
own.setUserId(23L);
|
||||||
|
when(candidateMapper.selectById(5L)).thenReturn(own);
|
||||||
|
|
||||||
|
service.deleteCandidate(23L, 5L);
|
||||||
|
|
||||||
|
verify(candidateMapper).deleteById(5L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matchShopsRejectsEmptyAfterNormalization() {
|
||||||
|
ProductRiskMatchShopsRequest request = new ProductRiskMatchShopsRequest();
|
||||||
|
request.setUserId(23L);
|
||||||
|
request.setShopNames(List.of(" ", ""));
|
||||||
|
|
||||||
|
assertThrows(BusinessException.class, () -> service.matchShops(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void matchShopsDeduplicatesAndKeepsOrder() {
|
||||||
|
ProductRiskMatchShopsRequest request = new ProductRiskMatchShopsRequest();
|
||||||
|
request.setUserId(23L);
|
||||||
|
request.setShopNames(List.of(" A ", "B", "A"));
|
||||||
|
when(shopSwitchService.findIndexedStoreByName(anyString(), anyBoolean())).thenAnswer(invocation ->
|
||||||
|
matched((String) invocation.getArgument(0)));
|
||||||
|
|
||||||
|
ProductRiskMatchShopsVo vo = service.matchShops(request);
|
||||||
|
|
||||||
|
assertEquals(2, vo.getItems().size(), "重复店铺名只匹配一次");
|
||||||
|
assertEquals("A", vo.getItems().get(0).getShopName());
|
||||||
|
assertEquals("B", vo.getItems().get(1).getShopName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
void countCandidatesTreatsNullCountAsZero() {
|
||||||
|
when(candidateMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(null);
|
||||||
|
|
||||||
|
assertEquals(0L, service.countCandidates(23L));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void countCandidatesRejectsInvalidUser() {
|
||||||
|
assertThrows(BusinessException.class, () -> service.countCandidates(-1L));
|
||||||
|
assertTrue(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user