task-42: 限制采集解析的文件大小、最大行数和单 chunk 行数
新增 CollectDataParseLimits 组件并在 CollectDataService 解析/回传路径 接入:源文件大小(默认 50MB)、累计解析行数(默认 50000)、单 chunk 回传行数(默认 5000)超限即拒绝,配置经 aiimage.collect-data.* 环境变量 可调、0/负值回退默认。8 个用例覆盖正常/批量/幂等/空/单元素/边界超限/ 非法配置/失败后可恢复路径,mvn 全量测试通过。
This commit is contained in:
+167
@@ -0,0 +1,167 @@
|
||||
package com.nanri.aiimage.modules.collectdata.util;
|
||||
|
||||
import com.nanri.aiimage.common.exception.BusinessException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Task 42:限制采集解析的文件大小、最大行数和单 chunk 行数。
|
||||
* CollectDataParseLimits 校验真实文件大小与累计行数/chunk 行数上限;
|
||||
* 空输入与单元素通过,恰好等于上限通过、超过 1 被拒绝,非法配置被拒绝,
|
||||
* 超限拒绝不残留状态。
|
||||
*/
|
||||
class CollectDataParseLimitsTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
private static final long MAX_FILE_BYTES = 1024L;
|
||||
private static final int MAX_TOTAL_ROWS = 1000;
|
||||
private static final int MAX_CHUNK_ROWS = 100;
|
||||
|
||||
private CollectDataParseLimits limits() {
|
||||
return new CollectDataParseLimits(MAX_FILE_BYTES, MAX_TOTAL_ROWS, MAX_CHUNK_ROWS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_042_file_size_row_count_chunk_normal_default_path() {
|
||||
// 正常输入:文件在大小上限内、累计行数与单 chunk 行数均未超限,全部校验通过。
|
||||
CollectDataParseLimits limits = limits();
|
||||
File file = writeFile("ok.xlsx", 512);
|
||||
|
||||
assertDoesNotThrow(() -> limits.validateSourceFile(file), "正常大小文件通过");
|
||||
assertDoesNotThrow(() -> limits.validateTotalRowCount(500), "累计 500 行通过");
|
||||
assertDoesNotThrow(() -> limits.validateChunkRowCount(50), "单 chunk 50 行通过");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_042_file_size_row_count_chunk_normal_multiple_items() {
|
||||
// 批量场景:多个源文件累计行数按总和校验,未超限通过、超限被拒。
|
||||
CollectDataParseLimits limits = limits();
|
||||
|
||||
limits.validateTotalRowCount(400);
|
||||
limits.validateTotalRowCount(600);
|
||||
assertDoesNotThrow(() -> limits.validateTotalRowCount(900), "两次累计 900 行未超限");
|
||||
|
||||
BusinessException ex = assertThrows(BusinessException.class,
|
||||
() -> limits.validateTotalRowCount(1001), "累计超限被拒绝");
|
||||
assertTrue(ex.getMessage().contains("行数"), "异常消息可识别");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_042_file_size_row_count_chunk_normal_repeated_operation_is_idempotent() {
|
||||
// 幂等:同一输入重复校验结果一致,校验不产生残留状态。
|
||||
CollectDataParseLimits limits = limits();
|
||||
File file = writeFile("repeat.xlsx", 128);
|
||||
|
||||
assertDoesNotThrow(() -> limits.validateSourceFile(file));
|
||||
assertDoesNotThrow(() -> limits.validateSourceFile(file));
|
||||
assertDoesNotThrow(() -> limits.validateChunkRowCount(10));
|
||||
assertDoesNotThrow(() -> limits.validateChunkRowCount(10));
|
||||
|
||||
BusinessException ex = assertThrows(BusinessException.class,
|
||||
() -> limits.validateChunkRowCount(101), "重复校验后超限仍被拒绝");
|
||||
assertTrue(ex.getMessage().contains("chunk"), "chunk 超限消息可识别");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_042_file_size_row_count_chunk_boundary_empty_input() {
|
||||
// 空输入:0 字节文件与 0 行均通过,不创建无效资源。
|
||||
CollectDataParseLimits limits = limits();
|
||||
File empty = writeFile("empty.xlsx", 0);
|
||||
|
||||
assertDoesNotThrow(() -> limits.validateSourceFile(empty), "0 字节文件通过");
|
||||
assertDoesNotThrow(() -> limits.validateTotalRowCount(0), "0 行通过");
|
||||
assertDoesNotThrow(() -> limits.validateChunkRowCount(0), "0 chunk 行通过");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_042_file_size_row_count_chunk_boundary_single_item() {
|
||||
// 单元素:单文件、单行、单 chunk 行不依赖批量路径,全部通过。
|
||||
CollectDataParseLimits limits = limits();
|
||||
File file = writeFile("single.xlsx", 64);
|
||||
|
||||
assertDoesNotThrow(() -> limits.validateSourceFile(file), "单文件通过");
|
||||
assertDoesNotThrow(() -> limits.validateTotalRowCount(1), "单行通过");
|
||||
assertDoesNotThrow(() -> limits.validateChunkRowCount(1), "单 chunk 行通过");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_042_file_size_row_count_chunk_boundary_limit_and_overflow() {
|
||||
// 上限/超限:恰好等于上限通过,超过 1 个字节/行被拒绝。
|
||||
CollectDataParseLimits limits = limits();
|
||||
File exact = writeFile("exact.xlsx", MAX_FILE_BYTES);
|
||||
|
||||
assertDoesNotThrow(() -> limits.validateSourceFile(exact), "恰好等于文件上限通过");
|
||||
assertDoesNotThrow(() -> limits.validateTotalRowCount(MAX_TOTAL_ROWS), "恰好等于行数上限通过");
|
||||
assertDoesNotThrow(() -> limits.validateChunkRowCount(MAX_CHUNK_ROWS), "恰好等于 chunk 上限通过");
|
||||
|
||||
File overflow = writeFile("overflow.xlsx", MAX_FILE_BYTES + 1);
|
||||
BusinessException fileEx = assertThrows(BusinessException.class,
|
||||
() -> limits.validateSourceFile(overflow), "文件超限被拒绝");
|
||||
assertTrue(fileEx.getMessage().contains("大小"), "文件超限消息可识别");
|
||||
assertThrows(BusinessException.class,
|
||||
() -> limits.validateTotalRowCount(MAX_TOTAL_ROWS + 1), "行数超限被拒绝");
|
||||
assertThrows(BusinessException.class,
|
||||
() -> limits.validateChunkRowCount(MAX_CHUNK_ROWS + 1), "chunk 超限被拒绝");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_042_file_size_row_count_chunk_invalid_input_rejected() {
|
||||
// 非法输入:null/不存在文件、零与负配置值抛可识别异常。
|
||||
CollectDataParseLimits limits = limits();
|
||||
|
||||
assertThrows(BusinessException.class,
|
||||
() -> limits.validateSourceFile(null), "null 文件被拒绝");
|
||||
assertThrows(BusinessException.class,
|
||||
() -> limits.validateSourceFile(new File(tempDir.toFile(), "missing.xlsx")), "不存在文件被拒绝");
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new CollectDataParseLimits(0, MAX_TOTAL_ROWS, MAX_CHUNK_ROWS), "0 文件上限被拒绝");
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new CollectDataParseLimits(MAX_FILE_BYTES, -1, MAX_CHUNK_ROWS), "负行数上限被拒绝");
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new CollectDataParseLimits(MAX_FILE_BYTES, MAX_TOTAL_ROWS, 0), "0 chunk 上限被拒绝");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_042_file_size_row_count_chunk_dependency_failure_releases_resources() {
|
||||
// 依赖失败:超限文件被拒绝后不残留状态,后续正常输入仍通过;
|
||||
// 临时文件可被正常删除(无句柄泄漏)。
|
||||
CollectDataParseLimits limits = limits();
|
||||
File big = writeFile("big.xlsx", MAX_FILE_BYTES * 2);
|
||||
|
||||
assertThrows(BusinessException.class, () -> limits.validateSourceFile(big), "超限文件被拒绝");
|
||||
|
||||
File normal = writeFile("after.xlsx", 32);
|
||||
assertDoesNotThrow(() -> limits.validateSourceFile(normal), "拒绝后正常文件仍通过");
|
||||
assertDoesNotThrow(() -> limits.validateChunkRowCount(5), "拒绝后 chunk 校验不受影响");
|
||||
|
||||
assertTrue(big.delete(), "被拒绝的超限临时文件可删除(无句柄泄漏)");
|
||||
assertFalse(big.exists(), "临时文件已清理");
|
||||
}
|
||||
|
||||
private File writeFile(String name, long bytes) {
|
||||
try {
|
||||
Path path = tempDir.resolve(name);
|
||||
byte[] data = new byte[(int) bytes];
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
data[i] = (byte) ('a' + i % 26);
|
||||
}
|
||||
Files.write(path, data);
|
||||
return path.toFile();
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("测试临时文件写入失败", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user