task-153: fileKey 兜底查找契约(保存即登记、重启后目录枚举重建、缺失明确失败、清理后不可找、语义稳定)+ 8 条测试

This commit is contained in:
2026-09-02 06:34:22 +08:00
parent 3b34ec3bef
commit 57f75b8bc3
@@ -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-153fileKey 兜底查找契约(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<String, String>) 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;
}
}