task-135: 任务状态成功时机契约(SUCCESS 必在文件生成上传后、无文件不成功、fileReady/downloadUrl 语义不变)+ 8 条守门测试
This commit is contained in:
+231
@@ -0,0 +1,231 @@
|
||||
package com.nanri.aiimage.modules.task.contract;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.nanri.aiimage.modules.collectdata.mapper.CollectDataCountryPrefMapper;
|
||||
import com.nanri.aiimage.modules.collectdata.mapper.CollectDataItemMapper;
|
||||
import com.nanri.aiimage.modules.collectdata.service.CollectDataExcelAssemblyService;
|
||||
import com.nanri.aiimage.modules.collectdata.service.CollectDataService;
|
||||
import com.nanri.aiimage.modules.collectdata.util.CollectDataBatchQuery;
|
||||
import com.nanri.aiimage.modules.collectdata.util.CollectDataBrandBatchFilter;
|
||||
import com.nanri.aiimage.modules.collectdata.util.CollectDataInvalidAsinBatchWriter;
|
||||
import com.nanri.aiimage.modules.collectdata.util.CollectDataResultItemBatchWriter;
|
||||
import com.nanri.aiimage.modules.collectdata.util.CollectDataResultDetailReader;
|
||||
import com.nanri.aiimage.modules.file.service.LocalFileStorageService;
|
||||
import com.nanri.aiimage.modules.file.service.oss.OssStorageService;
|
||||
import com.nanri.aiimage.modules.invalidasin.mapper.InvalidAsinDataMapper;
|
||||
import com.nanri.aiimage.modules.task.mapper.FileResultMapper;
|
||||
import com.nanri.aiimage.modules.task.mapper.FileTaskMapper;
|
||||
import com.nanri.aiimage.modules.task.mapper.TaskChunkMapper;
|
||||
import com.nanri.aiimage.modules.task.mapper.TaskResultItemMapper;
|
||||
import com.nanri.aiimage.modules.task.mapper.TaskScopeStateMapper;
|
||||
import com.nanri.aiimage.modules.task.model.entity.FileResultEntity;
|
||||
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
|
||||
import com.nanri.aiimage.modules.task.model.entity.TaskFileJobEntity;
|
||||
import com.nanri.aiimage.modules.task.service.TaskDistributedLockService;
|
||||
import com.nanri.aiimage.modules.task.service.TaskFileJobService;
|
||||
import com.nanri.aiimage.modules.task.service.TaskProgressLightAssembler;
|
||||
import com.nanri.aiimage.modules.task.service.TransientPayloadStorageService;
|
||||
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.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
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.eq;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* task-135:任务状态成功时机硬约束契约(spec 07 §2)。
|
||||
* 任务 SUCCESS 必须发生在结果文件生成并上传之后;无文件不成功;
|
||||
* fileReady = resultFileUrl 非空;downloadUrl = 上传 objectKey。collectdata 路径守门。
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SuccessTimingContractTest {
|
||||
|
||||
private static final Long TASK_ID = 6868L;
|
||||
private static final Long RESULT_ID = 6869L;
|
||||
|
||||
@Mock private LocalFileStorageService localFileStorageService;
|
||||
@Mock private FileTaskMapper fileTaskMapper;
|
||||
@Mock private FileResultMapper fileResultMapper;
|
||||
@Mock private CollectDataItemMapper collectDataItemMapper;
|
||||
@Mock private CollectDataCountryPrefMapper collectDataCountryPrefMapper;
|
||||
@Mock private InvalidAsinDataMapper invalidAsinDataMapper;
|
||||
@Mock private TaskChunkMapper taskChunkMapper;
|
||||
@Mock private TaskScopeStateMapper taskScopeStateMapper;
|
||||
@Mock private TaskResultItemMapper taskResultItemMapper;
|
||||
@Mock private TaskDistributedLockService taskDistributedLockService;
|
||||
@Mock private TaskFileJobService taskFileJobService;
|
||||
@Mock private TransientPayloadStorageService transientPayloadStorageService;
|
||||
@Mock private CollectDataExcelAssemblyService excelAssemblyService;
|
||||
@Mock private OssStorageService ossStorageService;
|
||||
@Mock private ObjectMapper objectMapper;
|
||||
@Mock private TransactionTemplate transactionTemplate;
|
||||
@Mock private CollectDataBatchQuery collectDataBatchQuery;
|
||||
@Mock private CollectDataBrandBatchFilter brandBatchFilter;
|
||||
@Mock private CollectDataInvalidAsinBatchWriter invalidAsinBatchWriter;
|
||||
@Mock private CollectDataResultItemBatchWriter resultItemBatchWriter;
|
||||
@Mock private CollectDataResultDetailReader resultDetailReader;
|
||||
|
||||
@InjectMocks private CollectDataService service;
|
||||
|
||||
@BeforeAll
|
||||
static void initializeMybatisMetadata() {
|
||||
MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
|
||||
TableInfoHelper.initTableInfo(assistant, FileTaskEntity.class);
|
||||
TableInfoHelper.initTableInfo(assistant, FileResultEntity.class);
|
||||
TableInfoHelper.initTableInfo(assistant, TaskFileJobEntity.class);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
lenient().when(fileTaskMapper.selectById(TASK_ID)).thenReturn(runningTask());
|
||||
lenient().when(fileResultMapper.selectById(RESULT_ID)).thenReturn(runningResult());
|
||||
lenient().when(taskResultItemMapper.selectList(any())).thenReturn(List.of());
|
||||
lenient().when(ossStorageService.uploadResultFile(any(), eq("COLLECT_DATA")))
|
||||
.thenReturn("oss://shufuai/collect-data/6868.xlsx");
|
||||
lenient().when(fileResultMapper.updateById(any(FileResultEntity.class))).thenReturn(1);
|
||||
lenient().when(fileTaskMapper.updateById(any(FileTaskEntity.class))).thenReturn(1);
|
||||
}
|
||||
|
||||
private TaskFileJobEntity job() {
|
||||
TaskFileJobEntity job = new TaskFileJobEntity();
|
||||
job.setId(901L);
|
||||
job.setTaskId(TASK_ID);
|
||||
job.setResultId(RESULT_ID);
|
||||
job.setModuleType("COLLECT_DATA");
|
||||
job.setJobType("ASSEMBLE_RESULT");
|
||||
job.setStatus("SUCCESS");
|
||||
return job;
|
||||
}
|
||||
|
||||
@Test
|
||||
void successHappensOnlyAfterFileGeneratedAndUploaded() {
|
||||
service.processResultFileJob(job());
|
||||
|
||||
var order = inOrder(ossStorageService, fileResultMapper, fileTaskMapper);
|
||||
order.verify(ossStorageService).uploadResultFile(any(), eq("COLLECT_DATA"));
|
||||
order.verify(fileResultMapper).updateById(any(FileResultEntity.class));
|
||||
order.verify(fileTaskMapper).updateById(any(FileTaskEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void taskStatusBecomesSuccessWithFileFields() {
|
||||
service.processResultFileJob(job());
|
||||
|
||||
ArgumentCaptor<FileTaskEntity> taskCaptor = ArgumentCaptor.forClass(FileTaskEntity.class);
|
||||
verify(fileTaskMapper).updateById(taskCaptor.capture());
|
||||
FileTaskEntity updated = taskCaptor.getValue();
|
||||
assertEquals("SUCCESS", updated.getStatus(), "任务状态成功时机 = 结果文件生成后");
|
||||
assertEquals(1, updated.getSuccessFileCount());
|
||||
assertEquals(0, updated.getFailedFileCount());
|
||||
assertNotNull(updated.getFinishedAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void resultCarriesDownloadUrlAndReadyState() {
|
||||
service.processResultFileJob(job());
|
||||
|
||||
ArgumentCaptor<FileResultEntity> resultCaptor = ArgumentCaptor.forClass(FileResultEntity.class);
|
||||
verify(fileResultMapper).updateById(resultCaptor.capture());
|
||||
FileResultEntity result = resultCaptor.getValue();
|
||||
assertEquals("oss://shufuai/collect-data/6868.xlsx", result.getResultFileUrl(),
|
||||
"downloadUrl = 上传后的 objectKey(含义不变)");
|
||||
assertEquals("采集-6868-result.xlsx", result.getResultFilename());
|
||||
assertEquals(1, result.getSuccess());
|
||||
assertTrue(result.getResultFileUrl() != null && !result.getResultFileUrl().isBlank(),
|
||||
"fileReady 语义:url 非空即可下载");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fileReadyDerivedFromResultFileUrl() {
|
||||
service.processResultFileJob(job());
|
||||
|
||||
// fileReady 由 url 推导(TaskProgressLightAssembler 语义),成功时机与 url 落库绑定
|
||||
verify(fileResultMapper).updateById(any(FileResultEntity.class));
|
||||
verify(fileTaskMapper).updateById(any(FileTaskEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void noFileGeneratedNoSuccess() {
|
||||
doThrow(new IllegalStateException("工作簿生成失败"))
|
||||
.when(excelAssemblyService).writeWorkbookSegmented(any(), any(), any(), any());
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> service.processResultFileJob(job()));
|
||||
|
||||
verify(fileTaskMapper, never()).updateById(any(FileTaskEntity.class));
|
||||
verify(ossStorageService, never()).uploadResultFile(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadFailureNoSuccess() {
|
||||
when(ossStorageService.uploadResultFile(any(), eq("COLLECT_DATA")))
|
||||
.thenThrow(new IllegalStateException("上传失败"));
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> service.processResultFileJob(job()));
|
||||
|
||||
verify(fileTaskMapper, never()).updateById(any(FileTaskEntity.class));
|
||||
verify(fileResultMapper, never()).updateById(any(FileResultEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void successObservableThroughResultRow() {
|
||||
service.processResultFileJob(job());
|
||||
|
||||
ArgumentCaptor<FileResultEntity> resultCaptor = ArgumentCaptor.forClass(FileResultEntity.class);
|
||||
verify(fileResultMapper).updateById(resultCaptor.capture());
|
||||
assertEquals(1, resultCaptor.getValue().getSuccess(), "result.success=1 供前端观测");
|
||||
assertEquals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
resultCaptor.getValue().getResultContentType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void successContractFrozen() {
|
||||
service.processResultFileJob(job());
|
||||
|
||||
// 契约快照:文件生成+上传 → result 落库 → 任务 SUCCESS(一次 updateById 各一)
|
||||
verify(ossStorageService).uploadResultFile(any(), eq("COLLECT_DATA"));
|
||||
verify(fileResultMapper).updateById(any(FileResultEntity.class));
|
||||
verify(fileTaskMapper).updateById(any(FileTaskEntity.class));
|
||||
verify(fileTaskMapper, org.mockito.Mockito.times(1)).updateById(any(FileTaskEntity.class));
|
||||
}
|
||||
|
||||
private FileTaskEntity runningTask() {
|
||||
FileTaskEntity task = new FileTaskEntity();
|
||||
task.setId(TASK_ID);
|
||||
task.setModuleType("COLLECT_DATA");
|
||||
task.setStatus("RUNNING");
|
||||
task.setUserId(7L);
|
||||
task.setResultJson("{}");
|
||||
return task;
|
||||
}
|
||||
|
||||
private FileResultEntity runningResult() {
|
||||
FileResultEntity result = new FileResultEntity();
|
||||
result.setId(RESULT_ID);
|
||||
result.setTaskId(TASK_ID);
|
||||
result.setModuleType("COLLECT_DATA");
|
||||
result.setSourceFilename("采集-6868");
|
||||
result.setSuccess(0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user