test(G4): 巡店删除 ResolveService 补 13 个契约测试(该模块此前零测试文件)

覆盖:用户校验、索引未命中/同名冲突拒绝、重复添加幂等、他人记录不可删(同文案防探测)、
匹配结果去重保序、count 空值兜底。
This commit is contained in:
2026-09-14 07:05:04 +08:00
parent 9ba231dc4c
commit da0f10f1cc
@@ -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);
}
}