diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/task/contract/PublishFileLevelStatusTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/task/contract/PublishFileLevelStatusTest.java new file mode 100644 index 00000000..784214a9 --- /dev/null +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/task/contract/PublishFileLevelStatusTest.java @@ -0,0 +1,170 @@ +package com.nanri.aiimage.modules.task.contract; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nanri.aiimage.config.InstanceMetadata; +import com.nanri.aiimage.modules.file.service.LocalFileStorageService; +import com.nanri.aiimage.modules.file.service.oss.OssStorageService; +import com.nanri.aiimage.modules.publish.mapper.PublishFileMapper; +import com.nanri.aiimage.modules.publish.mapper.PublishItemMapper; +import com.nanri.aiimage.modules.publish.model.vo.PublishResultVo; +import com.nanri.aiimage.modules.publish.service.PublishTaskService; +import com.nanri.aiimage.modules.publish.service.PublishWorkbookService; +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.TaskScopeStateMapper; +import com.nanri.aiimage.modules.task.model.entity.FileResultEntity; +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 com.nanri.aiimage.modules.ziniao.service.ZiniaoShopSwitchService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.transaction.support.TransactionTemplate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; + +/** + * task-143:publish 文件级 file 状态语义契约(plan 08)。 + * publish 的 file 状态为文件级(PublishResultVo.fileReady 由 result.success+url + * 推导,PublishItemsPageVo.fileStatus = file.status),与其余模块 job 级不同; + * 语义固化。 + */ +@ExtendWith(MockitoExtension.class) +class PublishFileLevelStatusTest { + + private static final Long RESULT_ID = 9201L; + + @Mock private LocalFileStorageService localFileStorageService; + @Mock private ZiniaoShopSwitchService ziniaoShopSwitchService; + @Mock private PublishWorkbookService workbookService; + @Mock private PublishFileMapper publishFileMapper; + @Mock private PublishItemMapper publishItemMapper; + @Mock private FileTaskMapper fileTaskMapper; + @Mock private FileResultMapper fileResultMapper; + @Mock private TaskChunkMapper taskChunkMapper; + @Mock private TaskScopeStateMapper taskScopeStateMapper; + @Mock private TaskFileJobService taskFileJobService; + @Mock private TaskDistributedLockService taskDistributedLockService; + @Mock private TransientPayloadStorageService transientPayloadStorageService; + @Mock private OssStorageService ossStorageService; + @Mock private ObjectMapper objectMapper; + @Mock private TransactionTemplate transactionTemplate; + @Mock private InstanceMetadata instanceMetadata; + @Mock private TaskProgressLightAssembler taskProgressLightAssembler; + + private PublishTaskService service; + + @BeforeEach + void setUp() { + service = new PublishTaskService( + localFileStorageService, ziniaoShopSwitchService, workbookService, publishFileMapper, + publishItemMapper, fileTaskMapper, fileResultMapper, taskChunkMapper, taskScopeStateMapper, + taskFileJobService, taskDistributedLockService, transientPayloadStorageService, ossStorageService, + objectMapper, transactionTemplate, instanceMetadata, taskProgressLightAssembler); + lenient().when(ossStorageService.generateFreshDownloadUrl(any())) + .thenReturn("https://dl.example/publish-result.zip"); + } + + private PublishResultVo toVo(FileResultEntity result, TaskFileJobEntity job) { + return (PublishResultVo) ReflectionTestUtils.invokeMethod(service, "toResultVo", result, job); + } + + private FileResultEntity result(int success, String url) { + FileResultEntity result = new FileResultEntity(); + result.setId(RESULT_ID); + result.setSuccess(success); + result.setResultFileUrl(url); + result.setResultFilename("PUBLISH-1_上架结果.zip"); + return result; + } + + private TaskFileJobEntity job() { + TaskFileJobEntity job = new TaskFileJobEntity(); + job.setId(9301L); + job.setStatus("RUNNING"); + job.setRetryCount(1); + job.setErrorMessage("写入慢"); + return job; + } + + @Test + void readyTrueWhenSuccessAndUrlPresent() { + PublishResultVo vo = toVo(result(1, "oss://result/p.zip"), null); + + assertTrue(vo.getFileReady()); + assertEquals("https://dl.example/publish-result.zip", vo.getDownloadUrl()); + } + + @Test + void readyFalseWhenUrlMissing() { + PublishResultVo vo = toVo(result(1, null), null); + + assertFalse(vo.getFileReady()); + assertNull(vo.getDownloadUrl(), "未 ready 不生成下载直链"); + } + + @Test + void failedFileNeverReady() { + PublishResultVo vo = toVo(result(0, "oss://result/p.zip"), null); + + assertFalse(vo.getFileReady(), "success=0 的文件不算 ready"); + assertNull(vo.getDownloadUrl()); + } + + @Test + void perFileReadyIsIndependent() { + PublishResultVo ready = toVo(result(1, "oss://a.zip"), null); + PublishResultVo pending = toVo(result(1, null), null); + + assertTrue(ready.getFileReady()); + assertFalse(pending.getFileReady()); + } + + @Test + void mixedFilesShowMixedReadyState() { + PublishResultVo failed = toVo(result(0, "oss://f.zip"), null); + PublishResultVo ready = toVo(result(1, "oss://r.zip"), null); + + assertFalse(failed.getFileReady()); + assertTrue(ready.getFileReady()); + } + + @Test + void noResultReturnsNull() { + assertNull(toVo(null, null), "无结果记录不返回 VO"); + } + + @Test + void jobFieldsMappedWhenJobPresent() { + PublishResultVo vo = toVo(result(1, "oss://p.zip"), job()); + + assertEquals(9301L, vo.getFileJobId()); + assertEquals("RUNNING", vo.getFileJobStatus()); + assertEquals(1, vo.getFileJobRetryCount()); + assertEquals("写入慢", vo.getFileJobError()); + } + + @Test + void semanticsFrozen() { + PublishResultVo vo = toVo(result(1, "oss://p.zip"), job()); + + // 快照:ready 由 result 行推导(非 job),job 字段独立携带(文件级语义) + assertTrue(vo.getFileReady()); + assertEquals("PUBLISH-1_上架结果.zip", vo.getResultFilename()); + assertEquals(RESULT_ID, vo.getResultId()); + assertEquals("RUNNING", vo.getFileJobStatus()); + } +}