test(G4): 定时匹配/格式转换/数据拆分 补 27 个契约测试(三个模块此前均零测试文件)

- ShopMatchResolveService 15:候选越权与幂等、匹配去重保序、国家偏好默认顺序与坏 JSON 兜底
- ConvertTemplateService 8:内置模板禁删(软禁用)、导入命名/后缀补全、设为默认时清掉其它默认位
- SplitRunService 4:下载/删除历史必须属于本人且模块匹配(含无结果文件不可下载)

至此审查点名的 6 个零测试模块全部有测试文件。
This commit is contained in:
2026-09-14 09:40:56 +08:00
parent ac36c08460
commit 1b480f915f
3 changed files with 542 additions and 0 deletions
@@ -0,0 +1,171 @@
package com.nanri.aiimage.modules.convert.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.modules.convert.mapper.ConvertTemplateMapper;
import com.nanri.aiimage.modules.convert.model.dto.ConvertTemplateImportRequest;
import com.nanri.aiimage.modules.convert.model.entity.ConvertTemplateEntity;
import com.nanri.aiimage.modules.convert.model.vo.ConvertTemplateVo;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
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.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* 格式转换的模板管理(2026-09 审查 G4:该模块此前零测试文件)。
*
* <p>重点:内置模板不可删(软禁用语义)、导入的自定义模板命名与文件名补全、
* 设为默认时其余模板的默认位必须被清掉。
*/
class ConvertTemplateServiceTest {
@BeforeAll
static void initializeMybatisMetadata() {
TableInfoHelper.initTableInfo(
new MapperBuilderAssistant(new MybatisConfiguration(), ""),
ConvertTemplateEntity.class);
}
private final ConvertTemplateMapper templateMapper = mock(ConvertTemplateMapper.class);
private final ConvertTemplateService service = new ConvertTemplateService(templateMapper);
private static ConvertTemplateEntity template(long id, String code, int isDefault, int builtIn) {
ConvertTemplateEntity entity = new ConvertTemplateEntity();
entity.setId(id);
entity.setTemplateCode(code);
entity.setTemplateName(code + "模板");
entity.setOutputFilename(code + ".txt");
entity.setIsDefault(isDefault);
entity.setBuiltIn(builtIn);
entity.setEnabled(1);
return entity;
}
@Test
@SuppressWarnings("unchecked")
void listEnabledTemplatesKeepsMapperOrder() {
when(templateMapper.selectList(any(LambdaQueryWrapper.class)))
.thenReturn(List.of(template(1L, "builtin_offer", 1, 1), template(2L, "user_1", 0, 0)));
List<ConvertTemplateVo> templates = service.listEnabledTemplates();
assertEquals(2, templates.size());
assertEquals("builtin_offer", templates.get(0).getTemplateCode());
assertTrue(templates.get(0).getIsDefault());
assertEquals("user_1", templates.get(1).getTemplateCode());
}
@Test
@SuppressWarnings("unchecked")
void getByCodeRejectsMissingOrDisabledTemplate() {
when(templateMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(null);
BusinessException exception = assertThrows(BusinessException.class, () -> service.getByCode("gone"));
assertEquals("模板不存在或已禁用", exception.getMessage());
}
@Test
void importTemplateRejectsBlankContent() {
ConvertTemplateImportRequest request = new ConvertTemplateImportRequest();
request.setTemplateName("我的模板");
request.setTemplateContent(" ");
assertThrows(BusinessException.class, () -> service.importTemplate(request));
verify(templateMapper, never()).insert(any(ConvertTemplateEntity.class));
}
@Test
void importTemplateGeneratesUserCodeAndTxtFilename() {
ConvertTemplateImportRequest request = new ConvertTemplateImportRequest();
request.setTemplateName("我的模板");
request.setTemplateContent("TemplateType=Offer\tVersion=1.4\n");
ConvertTemplateVo vo = service.importTemplate(request);
ArgumentCaptor<ConvertTemplateEntity> captor = ArgumentCaptor.forClass(ConvertTemplateEntity.class);
verify(templateMapper).insert(captor.capture());
ConvertTemplateEntity saved = captor.getValue();
assertTrue(saved.getTemplateCode().startsWith("user_"), "自定义模板编码带 user_ 前缀: " + saved.getTemplateCode());
assertEquals("我的模板.txt", saved.getOutputFilename(), "文件名补全 .txt 后缀");
assertEquals(0, saved.getBuiltIn(), "导入的模板不是内置模板");
assertEquals(0, saved.getIsDefault(), "导入不自动成为默认模板");
assertEquals(saved.getTemplateCode(), vo.getTemplateCode());
}
@Test
void importTemplateKeepsGivenTxtSuffixAndFallsBackToDefaultName() {
ConvertTemplateImportRequest request = new ConvertTemplateImportRequest();
request.setTemplateName("自定义.txt");
request.setTemplateContent("x");
service.importTemplate(request);
ArgumentCaptor<ConvertTemplateEntity> captor = ArgumentCaptor.forClass(ConvertTemplateEntity.class);
verify(templateMapper).insert(captor.capture());
assertEquals("自定义.txt", captor.getValue().getOutputFilename(), "已有 .txt 后缀不重复追加");
ConvertTemplateImportRequest unnamed = new ConvertTemplateImportRequest();
unnamed.setTemplateName(" ");
unnamed.setTemplateContent("x");
service.importTemplate(unnamed);
ArgumentCaptor<ConvertTemplateEntity> second = ArgumentCaptor.forClass(ConvertTemplateEntity.class);
verify(templateMapper, times(2)).insert(second.capture());
assertEquals("自定义 Offer 模板.txt", second.getAllValues().get(1).getOutputFilename());
}
@Test
@SuppressWarnings("unchecked")
void setDefaultTemplateClearsOthers() {
ConvertTemplateEntity target = template(2L, "user_1", 0, 0);
ConvertTemplateEntity other = template(3L, "user_2", 1, 0);
when(templateMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(target);
when(templateMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of(target, other));
service.setDefaultTemplate("user_1");
assertEquals(1, target.getIsDefault(), "目标模板置为默认");
assertEquals(0, other.getIsDefault(), "其余模板的默认位必须清掉,否则会同时存在多个默认模板");
verify(templateMapper, times(2)).updateById(any(ConvertTemplateEntity.class));
}
@Test
@SuppressWarnings("unchecked")
void deleteTemplateRejectsBuiltIn() {
when(templateMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(template(1L, "builtin_offer", 1, 1));
BusinessException exception = assertThrows(BusinessException.class,
() -> service.deleteTemplate("builtin_offer"));
assertEquals("内置模板不允许删除", exception.getMessage());
verify(templateMapper, never()).updateById(any(ConvertTemplateEntity.class));
}
@Test
@SuppressWarnings("unchecked")
void deleteTemplateSoftDisablesInsteadOfRemoving() {
ConvertTemplateEntity userTemplate = template(5L, "user_5", 0, 0);
when(templateMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(userTemplate);
service.deleteTemplate("user_5");
assertEquals(0, userTemplate.getEnabled(), "删除是软禁用(enabled=0),保留历史可追溯");
verify(templateMapper).updateById(userTemplate);
verify(templateMapper, never()).deleteById(any(Long.class));
}
}
@@ -0,0 +1,264 @@
package com.nanri.aiimage.modules.shopmatch.service;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.common.model.vo.ProductRiskCandidateVo;
import com.nanri.aiimage.common.model.vo.ProductRiskCountryPreferenceVo;
import com.nanri.aiimage.common.model.vo.ProductRiskMatchShopsVo;
import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskCandidateAddRequest;
import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskCountryPreferenceSaveRequest;
import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskMatchShopsRequest;
import com.nanri.aiimage.modules.shopmatch.mapper.ShopMatchCountryPrefMapper;
import com.nanri.aiimage.modules.shopmatch.mapper.ShopMatchShopCandidateMapper;
import com.nanri.aiimage.modules.shopmatch.model.entity.ShopMatchCountryPrefEntity;
import com.nanri.aiimage.modules.shopmatch.model.entity.ShopMatchShopCandidateEntity;
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 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.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:该模块此前零测试文件)。
*
* <p>重点:候选增删的越权保护与幂等、匹配去重保序、国家偏好的默认顺序与坏数据兜底。
*/
class ShopMatchResolveServiceTest {
@BeforeAll
static void initializeMybatisMetadata() {
MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
TableInfoHelper.initTableInfo(assistant, ShopMatchShopCandidateEntity.class);
TableInfoHelper.initTableInfo(assistant, ShopMatchCountryPrefEntity.class);
}
private final ShopMatchShopCandidateMapper candidateMapper = mock(ShopMatchShopCandidateMapper.class);
private final ShopMatchCountryPrefMapper countryPrefMapper = mock(ShopMatchCountryPrefMapper.class);
private final ObjectMapper objectMapper = new ObjectMapper();
private final ZiniaoShopSwitchService shopSwitchService = mock(ZiniaoShopSwitchService.class);
private ShopMatchResolveService service;
@BeforeEach
void setUp() {
service = new ShopMatchResolveService(candidateMapper, countryPrefMapper, objectMapper, shopSwitchService);
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 ProductRiskCountryPreferenceSaveRequest prefRequest(Long userId, List<String> codes) {
ProductRiskCountryPreferenceSaveRequest request = new ProductRiskCountryPreferenceSaveRequest();
request.setUserId(userId);
request.setCountryCodes(codes);
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));
}
@Test
@SuppressWarnings("unchecked")
void listCandidatesMapsRowsToViews() {
ShopMatchShopCandidateEntity row = new ShopMatchShopCandidateEntity();
row.setId(21L);
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(21L, 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(ShopMatchShopCandidateEntity.class));
}
@Test
void addCandidateRejectsWhenIndexMisses() {
ZiniaoShopMatchResultVo miss = new ZiniaoShopMatchResultVo();
miss.setMatched(false);
when(shopSwitchService.findIndexedStoreByName("未收录", false)).thenReturn(miss);
assertThrows(BusinessException.class, () -> service.addCandidate(addRequest(23L, "未收录")));
verify(candidateMapper, never()).insert(any(ShopMatchShopCandidateEntity.class));
}
@Test
void addCandidateRejectsConflictStatus() {
ZiniaoShopMatchResultVo conflict = matched("同名店铺");
conflict.setMatchStatus(ZiniaoShopIndexService.MATCH_STATUS_CONFLICT);
when(shopSwitchService.findIndexedStoreByName("同名店铺", false)).thenReturn(conflict);
assertThrows(BusinessException.class, () -> service.addCandidate(addRequest(23L, "同名店铺")));
}
@Test
@SuppressWarnings("unchecked")
void addCandidateIsIdempotentForExistingShop() {
when(shopSwitchService.findIndexedStoreByName("已存在", false)).thenReturn(matched("已存在"));
ShopMatchShopCandidateEntity existing = new ShopMatchShopCandidateEntity();
existing.setId(66L);
existing.setUserId(23L);
existing.setShopName("已存在");
existing.setCreatedAt(LocalDateTime.now());
when(candidateMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(existing);
ProductRiskCandidateVo vo = service.addCandidate(addRequest(23L, " 已存在 "));
assertEquals(66L, vo.getId());
verify(candidateMapper, never()).insert(any(ShopMatchShopCandidateEntity.class));
}
@Test
void deleteCandidateValidatesIdAndOwnership() {
assertThrows(BusinessException.class, () -> service.deleteCandidate(23L, null));
assertThrows(BusinessException.class, () -> service.deleteCandidate(23L, 0L));
ShopMatchShopCandidateEntity other = new ShopMatchShopCandidateEntity();
other.setId(8L);
other.setUserId(77L);
when(candidateMapper.selectById(8L)).thenReturn(other);
BusinessException exception = assertThrows(BusinessException.class,
() -> service.deleteCandidate(23L, 8L));
assertEquals("记录不存在", exception.getMessage(), "他人记录与不存在同文案,避免探测");
verify(candidateMapper, never()).deleteById(any(Long.class));
}
@Test
void deleteCandidateRemovesOwnRecord() {
ShopMatchShopCandidateEntity own = new ShopMatchShopCandidateEntity();
own.setId(8L);
own.setUserId(23L);
when(candidateMapper.selectById(8L)).thenReturn(own);
service.deleteCandidate(23L, 8L);
verify(candidateMapper).deleteById(8L);
}
@Test
void matchShopsRejectsEmptyAfterNormalization() {
ProductRiskMatchShopsRequest request = new ProductRiskMatchShopsRequest();
request.setUserId(23L);
request.setShopNames(List.of(" "));
assertThrows(BusinessException.class, () -> service.matchShops(request));
}
@Test
void matchShopsDeduplicatesKeepingFirstOccurrenceOrder() {
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
void countryPreferenceFallsBackToDefaultOrderWhenAbsent() {
when(countryPrefMapper.selectById(23L)).thenReturn(null);
ProductRiskCountryPreferenceVo vo = service.getCountryPreference(23L);
assertEquals(ShopMatchResolveService.DEFAULT_COUNTRY_PREFERENCE_ORDER, vo.getCountryCodes());
}
@Test
void countryPreferenceFallsBackToDefaultOrderOnBrokenJson() {
ShopMatchCountryPrefEntity broken = new ShopMatchCountryPrefEntity();
broken.setUserId(23L);
broken.setCountryCodesJson("{not-json");
when(countryPrefMapper.selectById(23L)).thenReturn(broken);
ProductRiskCountryPreferenceVo vo = service.getCountryPreference(23L);
assertEquals(ShopMatchResolveService.DEFAULT_COUNTRY_PREFERENCE_ORDER, vo.getCountryCodes(),
"坏数据不能让页面空白,回退默认顺序");
}
@Test
void countryPreferenceReadsStoredOrder() {
ShopMatchCountryPrefEntity stored = new ShopMatchCountryPrefEntity();
stored.setUserId(23L);
stored.setCountryCodesJson("[\"UK\",\"DE\"]");
when(countryPrefMapper.selectById(23L)).thenReturn(stored);
ProductRiskCountryPreferenceVo vo = service.getCountryPreference(23L);
assertEquals(List.of("UK", "DE"), vo.getCountryCodes(), "已保存顺序优先于默认顺序");
}
@Test
void saveCountryPreferenceRejectsEmptySelection() {
assertThrows(BusinessException.class, () -> service.saveCountryPreference(prefRequest(23L, List.of())));
assertThrows(BusinessException.class, () -> service.saveCountryPreference(prefRequest(23L, null)));
}
@Test
void saveCountryPreferenceInsertsThenUpdates() {
when(countryPrefMapper.selectById(23L)).thenReturn(null);
service.saveCountryPreference(prefRequest(23L, List.of("UK", "DE")));
verify(countryPrefMapper).insert(any(ShopMatchCountryPrefEntity.class));
ShopMatchCountryPrefEntity existing = new ShopMatchCountryPrefEntity();
existing.setUserId(23L);
existing.setCountryCodesJson("[\"DE\"]");
when(countryPrefMapper.selectById(23L)).thenReturn(existing);
ProductRiskCountryPreferenceVo updated = service.saveCountryPreference(prefRequest(23L, List.of("DE", "UK")));
assertEquals(List.of("DE", "UK"), updated.getCountryCodes());
verify(countryPrefMapper).updateById(existing);
}
}
@@ -0,0 +1,107 @@
package com.nanri.aiimage.modules.split.service;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.config.StorageProperties;
import com.nanri.aiimage.modules.file.service.oss.OssStorageService;
import com.nanri.aiimage.modules.task.mapper.FileResultMapper;
import com.nanri.aiimage.modules.task.mapper.FileTaskMapper;
import com.nanri.aiimage.modules.task.model.entity.FileResultEntity;
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 static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
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:该模块此前零测试文件)。
*
* <p>只覆盖"能安全断言、且与安全相关"的部分:下载与删除的历史记录必须属于本人且模块匹配。
* `run(...)` 依赖真实 workbook 与 OSS 上传,另需集成环境,此处不构造。
*/
class SplitRunServiceTest {
private static final String MODULE_TYPE = "SPLIT";
@BeforeAll
static void initializeMybatisMetadata() {
TableInfoHelper.initTableInfo(
new MapperBuilderAssistant(new MybatisConfiguration(), ""),
FileResultEntity.class);
}
private final FileTaskMapper fileTaskMapper = mock(FileTaskMapper.class);
private final FileResultMapper fileResultMapper = mock(FileResultMapper.class);
private final StorageProperties storageProperties = mock(StorageProperties.class);
private final OssStorageService ossStorageService = mock(OssStorageService.class);
private SplitRunService service;
@BeforeEach
void setUp() {
service = new SplitRunService(fileTaskMapper, fileResultMapper, storageProperties, ossStorageService);
}
private static FileResultEntity result(Long id, String moduleType, Long userId, String url) {
FileResultEntity entity = new FileResultEntity();
entity.setId(id);
entity.setModuleType(moduleType);
entity.setUserId(userId);
entity.setResultFileUrl(url);
return entity;
}
@Test
void deleteHistoryRejectsMissingOrForeignOrOtherModuleRecord() {
when(fileResultMapper.selectById(99L)).thenReturn(null);
assertThrows(BusinessException.class, () -> service.deleteHistory(99L, 23L), "记录不存在");
when(fileResultMapper.selectById(100L)).thenReturn(result(100L, MODULE_TYPE, 88L, "oss/x.xlsx"));
assertThrows(BusinessException.class, () -> service.deleteHistory(100L, 23L), "他人记录不可删");
when(fileResultMapper.selectById(101L)).thenReturn(result(101L, "DEDUPE", 23L, "oss/x.xlsx"));
assertThrows(BusinessException.class, () -> service.deleteHistory(101L, 23L), "跨模块记录不可删");
verify(fileResultMapper, never()).deleteById(anyLong());
}
@Test
void deleteHistoryRemovesOwnSplitRecord() {
when(fileResultMapper.selectById(102L)).thenReturn(result(102L, MODULE_TYPE, 23L, "oss/x.xlsx"));
service.deleteHistory(102L, 23L);
verify(fileResultMapper).deleteById(102L);
}
@Test
void downloadUrlRejectsForeignOrMissingFile() {
when(fileResultMapper.selectById(200L)).thenReturn(result(200L, MODULE_TYPE, 88L, "oss/x.xlsx"));
assertThrows(BusinessException.class, () -> service.getResultDownloadUrl(200L, 23L));
when(fileResultMapper.selectById(201L)).thenReturn(result(201L, MODULE_TYPE, 23L, null));
assertThrows(BusinessException.class, () -> service.getResultDownloadUrl(201L, 23L), "无结果文件时不可下载");
when(fileResultMapper.selectById(202L)).thenReturn(result(202L, "CONVERT", 23L, "oss/x.xlsx"));
assertThrows(BusinessException.class, () -> service.getResultDownloadUrl(202L, 23L));
verify(ossStorageService, never()).generateFreshDownloadUrl(any());
}
@Test
void downloadUrlReturnsFreshSignedUrlForOwnRecord() {
when(fileResultMapper.selectById(203L)).thenReturn(result(203L, MODULE_TYPE, 23L, "oss/split.xlsx"));
when(ossStorageService.generateFreshDownloadUrl("oss/split.xlsx")).thenReturn("https://oss.example/split.xlsx?sig=1");
assertEquals("https://oss.example/split.xlsx?sig=1", service.getResultDownloadUrl(203L, 23L));
}
}