diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/file/service/LocalFileKeyLookupTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/file/service/LocalFileKeyLookupTest.java new file mode 100644 index 00000000..d4455ef9 --- /dev/null +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/file/service/LocalFileKeyLookupTest.java @@ -0,0 +1,121 @@ +package com.nanri.aiimage.modules.file.service; + +import com.nanri.aiimage.config.StorageProperties; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.mock.web.MockMultipartFile; + +import java.io.File; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * task-153:fileKey 兜底查找契约(plan 09)。 + * 上传记录登记内存索引;进程重启后索引为空,按 fileKey 目录枚举兜底重建; + * 找不到时明确返回 null(调用方明确失败),不静默。 + */ +class LocalFileKeyLookupTest { + + @TempDir + Path tempDir; + + private LocalFileStorageService service; + + @BeforeEach + void setUp() { + StorageProperties properties = new StorageProperties(); + properties.setLocalTempDir(tempDir.toString()); + service = new LocalFileStorageService(properties); + } + + @Test + void fileKeyFoundAfterSave() throws Exception { + var vo = service.saveTempFile(new MockMultipartFile("f", "a.xlsx", "application/octet-stream", new byte[]{1, 2}), "upload"); + + File found = service.findLocalSourceFile(vo.getFileKey()); + + assertNotNull(found, "保存后按 fileKey 必须找到"); + assertTrue(found.isFile()); + assertEquals(vo.getFileKey(), found.getName().substring(0, vo.getFileKey().length())); + } + + @Test + void fileKeyFoundAfterRestart() throws Exception { + var vo = service.saveTempFile(new MockMultipartFile("f", "b.xlsx", "application/octet-stream", new byte[]{3}), "upload"); + // 模拟重启:新实例(内存索引为空)→ 目录枚举兜底 + StorageProperties properties = new StorageProperties(); + properties.setLocalTempDir(tempDir.toString()); + LocalFileStorageService restarted = new LocalFileStorageService(properties); + + File found = restarted.findLocalSourceFile(vo.getFileKey()); + + assertNotNull(found, "重启后按 fileKey 目录枚举兜底必须找到"); + } + + @Test + void missingFileKeyFailsExplicitly() { + assertNull(service.findLocalSourceFile("0123456789abcdef0123456789abcdef"), + "不存在的 fileKey 明确返回 null"); + } + + @Test + void nullAndBlankFileKeyAreSafe() { + assertNull(service.findLocalSourceFile(null)); + assertNull(service.findLocalSourceFile(" ")); + assertNull(service.findLocalSourceFile("")); + } + + @Test + void deletedFileKeyNotFindable() throws Exception { + var vo = service.saveTempFile(new MockMultipartFile("f", "c.xlsx", "application/octet-stream", new byte[]{4}), "upload"); + File found = service.findLocalSourceFile(vo.getFileKey()); + assertNotNull(found); + assertTrue(found.delete(), "清理测试文件"); + + assertNull(service.findLocalSourceFile(vo.getFileKey()), "文件清理后不可再找到"); + } + + @Test + void indexRecordPersistsForLookup() throws Exception { + var vo = service.saveTempFile(new MockMultipartFile("f", "d.xlsx", "application/octet-stream", new byte[]{5}), "upload"); + + File found = service.findLocalSourceFile(vo.getFileKey()); + + assertNotNull(found); + assertEquals(vo.getLocalPath(), found.getAbsolutePath(), "索引命中返回登记路径"); + } + + @Test + @SuppressWarnings("unchecked") + void indexRebuiltByDirectoryEnumeration() throws Exception { + var vo = service.saveTempFile(new MockMultipartFile("f", "e.xlsx", "application/octet-stream", new byte[]{6}), "upload"); + // 清空内存索引(模拟索引丢失),目录枚举仍可重建 + ((java.util.Map) org.springframework.test.util.ReflectionTestUtils + .getField(service, "sourceFileIndex")).clear(); + + File found = service.findLocalSourceFile(vo.getFileKey()); + + assertNotNull(found, "索引清空后目录枚举兜底重建"); + } + + @Test + void semanticsFrozen() throws Exception { + var vo = service.saveTempFile(new MockMultipartFile("f", "f.xlsx", "application/octet-stream", new byte[]{7}), "upload"); + File found = service.findLocalSourceFile(vo.getFileKey()); + File restartFound = new LocalFileStorageService(properties()).findLocalSourceFile(vo.getFileKey()); + + assertEquals(found.getAbsolutePath(), restartFound.getAbsolutePath(), + "同 fileKey 在重启前后解析到同一文件(语义不变)"); + } + + private StorageProperties properties() { + StorageProperties properties = new StorageProperties(); + properties.setLocalTempDir(tempDir.toString()); + return properties; + } +}