|
|
@@ -0,0 +1,136 @@
|
|
|
+package com.zsjz.ai.module.dm.service;
|
|
|
+
|
|
|
+import com.zsjz.ai.common.enums.FileSheetStateEnum;
|
|
|
+import com.zsjz.ai.common.exception.ServerException;
|
|
|
+import com.zsjz.ai.common.model.dm.entity.FileInfo;
|
|
|
+import com.zsjz.ai.module.dm.mapper.FileInfoMapper;
|
|
|
+import org.junit.jupiter.api.DisplayName;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.springframework.mock.web.MockMultipartFile;
|
|
|
+import org.springframework.test.util.ReflectionTestUtils;
|
|
|
+
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+
|
|
|
+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.assertThrows;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
+import static org.mockito.ArgumentMatchers.anyCollection;
|
|
|
+import static org.mockito.Mockito.mock;
|
|
|
+import static org.mockito.Mockito.never;
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
+
|
|
|
+/**
|
|
|
+ * {@link DmService#preProcessUploadedFile} 的「后缀分流」契约测试。
|
|
|
+ *
|
|
|
+ * <p>锁死的行为:**只有 csv / xls / xlsx 真正上传(落盘 + 解析);
|
|
|
+ * 其余后缀不落盘、不解析,只在 {@code file_info} 登记一条「不支持解析」的记录**。
|
|
|
+ *
|
|
|
+ * <p>为什么不需要 Spring 上下文 / 真实案件库:
|
|
|
+ * <ul>
|
|
|
+ * <li>不支持的后缀在 {@code saveUploadedFile} **之前**就返回了,整条路径不碰
|
|
|
+ * {@code StateManager}、不碰 DuckDB;</li>
|
|
|
+ * <li>反过来,「表格后缀会走到落盘」这一点用<b>未开案时抛「请先打开案件」</b>来反证 ——
|
|
|
+ * 异常本身就是「确实进了上传分支」的证据。</li>
|
|
|
+ * </ul>
|
|
|
+ * 因此本类可以纯 mock 跑,秒级完成。
|
|
|
+ */
|
|
|
+class DmServiceUploadSuffixTest {
|
|
|
+
|
|
|
+ private static final long BATCH_ID = 20260920L;
|
|
|
+
|
|
|
+ /** 与被测服务同包,直接 new;只有 fileInfoMapper 需要注入 */
|
|
|
+ private FileInfoMapper fileInfoMapper;
|
|
|
+
|
|
|
+ private DmService newService() {
|
|
|
+ DmService service = new DmService();
|
|
|
+ fileInfoMapper = mock(FileInfoMapper.class);
|
|
|
+ ReflectionTestUtils.setField(service, "fileInfoMapper", fileInfoMapper);
|
|
|
+ return service;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static MockMultipartFile upload(String fileName, String content) {
|
|
|
+ return new MockMultipartFile("file", fileName, null, content.getBytes(StandardCharsets.UTF_8));
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 不支持解析的分支 ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("docx:不落盘不解析,只登记一条「不支持解析」记录")
|
|
|
+ void unsupportedSuffixIsRegisteredOnly() {
|
|
|
+ DmService service = newService();
|
|
|
+
|
|
|
+ FileInfo result = service.preProcessUploadedFile(upload("报告.docx", "hello"), BATCH_ID, null);
|
|
|
+
|
|
|
+ assertNotNull(result, "不支持的后缀也要返回节点,前端才能在列表里显示失败原因");
|
|
|
+ assertEquals(FileSheetStateEnum.FILE_UNSUPPORTED_FAIL, result.getFileStatus());
|
|
|
+ assertEquals("报告.docx", result.getFileName());
|
|
|
+ assertEquals("docx", result.getFileType(), "fileType 记真实后缀,前端名称列会显示成「报告.docx · docx」");
|
|
|
+ assertEquals(Long.valueOf(0L), result.getPid(), "必须是文件根节点,否则列表里渲染不出来");
|
|
|
+ assertEquals(Long.valueOf(5L), result.getFileSize(), "文件未落盘,大小取浏览器实际传上来的字节数");
|
|
|
+ assertEquals(Long.valueOf(BATCH_ID), result.getBatchId());
|
|
|
+ assertTrue(result.getChildren().isEmpty(), "不支持解析的文件不会有 sheet 子节点");
|
|
|
+ assertNull(result.getFilePath(), "文件未落盘,filePath 必须为空 —— 下载接口据此拒绝下载");
|
|
|
+
|
|
|
+ verify(fileInfoMapper).insert(anyCollection());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("无后缀文件:同样登记为「不支持解析」,fileType 落成 unknown")
|
|
|
+ void fileWithoutSuffixIsRegisteredOnly() {
|
|
|
+ DmService service = newService();
|
|
|
+
|
|
|
+ FileInfo result = service.preProcessUploadedFile(upload("README", "x"), BATCH_ID, null);
|
|
|
+
|
|
|
+ assertEquals(FileSheetStateEnum.FILE_UNSUPPORTED_FAIL, result.getFileStatus());
|
|
|
+ assertEquals("unknown", result.getFileType());
|
|
|
+ assertNull(result.getFilePath());
|
|
|
+ verify(fileInfoMapper).insert(anyCollection());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("txt / pdf / md:一律只登记不落盘")
|
|
|
+ void otherCommonSuffixesAreRegisteredOnly() {
|
|
|
+ for (String name : new String[]{"日志.txt", "起诉书.pdf", "说明.md", "数据.json", "打包.zip"}) {
|
|
|
+ DmService service = newService();
|
|
|
+
|
|
|
+ FileInfo result = service.preProcessUploadedFile(upload(name, "x"), BATCH_ID, null);
|
|
|
+
|
|
|
+ assertEquals(FileSheetStateEnum.FILE_UNSUPPORTED_FAIL, result.getFileStatus(), name + " 应登记为不支持解析");
|
|
|
+ assertNull(result.getFilePath(), name + " 不应落盘");
|
|
|
+ verify(fileInfoMapper).insert(anyCollection());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 会真正上传的分支(用未开案异常反证) ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("xlsx / xls / csv:进入上传分支(未开案时抛「请先打开案件」,且不写文件表)")
|
|
|
+ void tableSuffixesEnterUploadPath() {
|
|
|
+ for (String name : new String[]{"数据.xlsx", "数据.xls", "数据.csv", "大写.CSV"}) {
|
|
|
+ DmService service = newService();
|
|
|
+
|
|
|
+ ServerException e = assertThrows(ServerException.class,
|
|
|
+ () -> service.preProcessUploadedFile(upload(name, "a,b"), BATCH_ID, null),
|
|
|
+ name + " 应进入落盘分支");
|
|
|
+
|
|
|
+ assertTrue(e.getMessage().contains("请先打开案件"),
|
|
|
+ name + " 的报错应来自 saveUploadedFile 的开案校验,实际: " + e.getMessage());
|
|
|
+ // 走了上传分支就不该在文件表里留「不支持解析」的记录
|
|
|
+ verify(fileInfoMapper, never()).insert(anyCollection());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("空文件:直接拒绝,不登记也不落盘")
|
|
|
+ void emptyFileIsRejected() {
|
|
|
+ DmService service = newService();
|
|
|
+
|
|
|
+ ServerException e = assertThrows(ServerException.class,
|
|
|
+ () -> service.preProcessUploadedFile(upload("空.xlsx", ""), BATCH_ID, null));
|
|
|
+
|
|
|
+ assertTrue(e.getMessage().contains("上传文件为空"));
|
|
|
+ verify(fileInfoMapper, never()).insert(anyCollection());
|
|
|
+ }
|
|
|
+}
|