From 57f75b8bc3151ecafa8d780630e0d5585f738842 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Wed, 2 Sep 2026 06:34:22 +0800 Subject: [PATCH] =?UTF-8?q?task-153:=20fileKey=20=E5=85=9C=E5=BA=95?= =?UTF-8?q?=E6=9F=A5=E6=89=BE=E5=A5=91=E7=BA=A6=EF=BC=88=E4=BF=9D=E5=AD=98?= =?UTF-8?q?=E5=8D=B3=E7=99=BB=E8=AE=B0=E3=80=81=E9=87=8D=E5=90=AF=E5=90=8E?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E6=9E=9A=E4=B8=BE=E9=87=8D=E5=BB=BA=E3=80=81?= =?UTF-8?q?=E7=BC=BA=E5=A4=B1=E6=98=8E=E7=A1=AE=E5=A4=B1=E8=B4=A5=E3=80=81?= =?UTF-8?q?=E6=B8=85=E7=90=86=E5=90=8E=E4=B8=8D=E5=8F=AF=E6=89=BE=E3=80=81?= =?UTF-8?q?=E8=AF=AD=E4=B9=89=E7=A8=B3=E5=AE=9A=EF=BC=89+=208=20=E6=9D=A1?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../file/service/LocalFileKeyLookupTest.java | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 backend-java/src/test/java/com/nanri/aiimage/modules/file/service/LocalFileKeyLookupTest.java 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; + } +}