fix: 完善工作流公共接口上传与错误契约
- 区分 HTTP 请求标识与内部上传标识,补齐 Redis 旧记录兼容和关联日志 - 保持归一化 MIME 一致,并隔离工作流鉴权错误契约对其他公共接口的影响 - 收口 Multipart 操作日志与对象存储故障分类,归档范围:S05
This commit is contained in:
@@ -25,6 +25,8 @@ import java.util.Set;
|
||||
*/
|
||||
public class WorkflowApiUploadLifecycleServiceTest {
|
||||
|
||||
private static final String HTTP_REQUEST_ID = "http-request-1";
|
||||
|
||||
/**
|
||||
* 验证同名文件 Part 会按顺序保存并注入文件对象数组。
|
||||
*/
|
||||
@@ -63,11 +65,12 @@ public class WorkflowApiUploadLifecycleServiceTest {
|
||||
secondHandle.encodeLocator()));
|
||||
|
||||
WorkflowApiPreparedUpload prepared = fixture.service.prepare(
|
||||
HTTP_REQUEST_ID,
|
||||
"flow",
|
||||
Map.of("user_input", "解析"),
|
||||
Map.of("documents", List.of(first, second)));
|
||||
|
||||
Assert.assertNotNull(prepared.getRequestId());
|
||||
Assert.assertNotNull(prepared.getUploadId());
|
||||
Assert.assertEquals("解析", prepared.getVariables().get("user_input"));
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> documents =
|
||||
@@ -85,6 +88,12 @@ public class WorkflowApiUploadLifecycleServiceTest {
|
||||
ArgumentCaptor.forClass(WorkflowApiUploadRecord.class);
|
||||
Mockito.verify(fixture.uploadStore).create(
|
||||
recordCaptor.capture());
|
||||
Assert.assertEquals(
|
||||
HTTP_REQUEST_ID,
|
||||
recordCaptor.getValue().getRequestId());
|
||||
Assert.assertNotEquals(
|
||||
recordCaptor.getValue().getRequestId(),
|
||||
recordCaptor.getValue().getUploadId());
|
||||
Assert.assertEquals(
|
||||
List.of("/files/first.pdf", "/files/second.docx"),
|
||||
recordCaptor.getValue().getStoredFiles().stream()
|
||||
@@ -130,6 +139,7 @@ public class WorkflowApiUploadLifecycleServiceTest {
|
||||
handle.encodeLocator()));
|
||||
|
||||
WorkflowApiPreparedUpload prepared = fixture.service.prepare(
|
||||
HTTP_REQUEST_ID,
|
||||
"flow",
|
||||
Map.of(),
|
||||
Map.of("documents", List.of(file)));
|
||||
@@ -187,6 +197,7 @@ public class WorkflowApiUploadLifecycleServiceTest {
|
||||
BusinessException exception = Assert.assertThrows(
|
||||
BusinessException.class,
|
||||
() -> fixture.service.prepare(
|
||||
HTTP_REQUEST_ID,
|
||||
"flow",
|
||||
Map.of(),
|
||||
Map.of("documents", List.of(file))));
|
||||
@@ -232,6 +243,7 @@ public class WorkflowApiUploadLifecycleServiceTest {
|
||||
BusinessException exception = Assert.assertThrows(
|
||||
BusinessException.class,
|
||||
() -> fixture.service.prepare(
|
||||
HTTP_REQUEST_ID,
|
||||
"flow",
|
||||
Map.of(),
|
||||
Map.of("documents", List.of(file))));
|
||||
@@ -245,6 +257,53 @@ public class WorkflowApiUploadLifecycleServiceTest {
|
||||
.remove(Mockito.any(WorkflowApiUploadRecord.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证无法解析的存储主机按配置故障返回不可重试的 50001。
|
||||
*/
|
||||
@Test
|
||||
public void prepareShouldTreatUnknownStorageHostAsPermanentFailure() {
|
||||
Fixture fixture = fixture();
|
||||
MultipartFile file = file(
|
||||
"report.pdf",
|
||||
"application/pdf",
|
||||
10L);
|
||||
Mockito.when(fixture.parameterResolver
|
||||
.resolveFileParameterNames("flow"))
|
||||
.thenReturn(Set.of("documents"));
|
||||
Mockito.when(fixture.parameterResolver.normalizeRuntimeVariables(
|
||||
Mockito.eq("flow"),
|
||||
Mockito.anyMap()))
|
||||
.thenAnswer(invocation -> new LinkedHashMap<>(
|
||||
invocation.getArgument(1)));
|
||||
FileStorageWriteHandle handle = handle("report.pdf");
|
||||
Mockito.when(fixture.fileStorageService.prepareRecoverableWrite(
|
||||
Mockito.anyString(),
|
||||
Mockito.anyString()))
|
||||
.thenReturn(handle);
|
||||
Mockito.when(fixture.fileStorageService.saveRecoverable(
|
||||
file,
|
||||
handle))
|
||||
.thenThrow(new IllegalStateException(
|
||||
"storage endpoint unavailable",
|
||||
new java.net.UnknownHostException(
|
||||
"invalid-storage-host")));
|
||||
|
||||
BusinessException exception = Assert.assertThrows(
|
||||
BusinessException.class,
|
||||
() -> fixture.service.prepare(
|
||||
HTTP_REQUEST_ID,
|
||||
"flow",
|
||||
Map.of(),
|
||||
Map.of("documents", List.of(file))));
|
||||
|
||||
Assert.assertEquals(500, exception.getHttpStatus());
|
||||
Assert.assertEquals(50001, exception.getErrorCode());
|
||||
Assert.assertFalse(exception.getMessage().contains(
|
||||
"invalid-storage-host"));
|
||||
Mockito.verify(fixture.fileStorageService)
|
||||
.deleteRecoverable(handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证未知文件 Part 在写入存储前被拒绝。
|
||||
*/
|
||||
@@ -258,6 +317,7 @@ public class WorkflowApiUploadLifecycleServiceTest {
|
||||
BusinessException exception = Assert.assertThrows(
|
||||
BusinessException.class,
|
||||
() -> fixture.service.prepare(
|
||||
HTTP_REQUEST_ID,
|
||||
"flow",
|
||||
Map.of(),
|
||||
Map.of("unknown", List.of(file))));
|
||||
@@ -284,6 +344,7 @@ public class WorkflowApiUploadLifecycleServiceTest {
|
||||
BusinessException exception = Assert.assertThrows(
|
||||
BusinessException.class,
|
||||
() -> fixture.service.prepare(
|
||||
HTTP_REQUEST_ID,
|
||||
"flow",
|
||||
Map.of(),
|
||||
Map.of("documents", List.of(file))));
|
||||
@@ -313,6 +374,7 @@ public class WorkflowApiUploadLifecycleServiceTest {
|
||||
BusinessException exception = Assert.assertThrows(
|
||||
BusinessException.class,
|
||||
() -> fixture.service.prepare(
|
||||
HTTP_REQUEST_ID,
|
||||
"flow",
|
||||
Map.of(),
|
||||
Map.of("documents", List.of(file))));
|
||||
@@ -356,6 +418,7 @@ public class WorkflowApiUploadLifecycleServiceTest {
|
||||
IllegalStateException exception = Assert.assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> fixture.service.prepare(
|
||||
HTTP_REQUEST_ID,
|
||||
"flow",
|
||||
Map.of(),
|
||||
Map.of("documents", List.of(file))));
|
||||
@@ -374,7 +437,8 @@ public class WorkflowApiUploadLifecycleServiceTest {
|
||||
public void abortShouldDeleteStoredFilesAndRecord() {
|
||||
Fixture fixture = fixture();
|
||||
WorkflowApiUploadRecord record = new WorkflowApiUploadRecord();
|
||||
record.setRequestId("request-1");
|
||||
record.setUploadId("upload-1");
|
||||
record.setRequestId(HTTP_REQUEST_ID);
|
||||
FileStorageWriteHandle firstHandle = handle("a.pdf");
|
||||
FileStorageWriteHandle secondHandle = handle("b.pdf");
|
||||
record.setStoredFiles(List.of(
|
||||
@@ -391,10 +455,10 @@ public class WorkflowApiUploadLifecycleServiceTest {
|
||||
Mockito.any(),
|
||||
Mockito.any()))
|
||||
.thenReturn(handle);
|
||||
Mockito.when(fixture.uploadStore.find("request-1"))
|
||||
Mockito.when(fixture.uploadStore.find("upload-1"))
|
||||
.thenReturn(Optional.of(record));
|
||||
|
||||
fixture.service.abort("request-1");
|
||||
fixture.service.abort("upload-1");
|
||||
|
||||
Mockito.verify(fixture.fileStorageService)
|
||||
.deleteRecoverable(firstHandle);
|
||||
@@ -486,16 +550,17 @@ public class WorkflowApiUploadLifecycleServiceTest {
|
||||
/**
|
||||
* 创建包含单个临时文件的上传记录。
|
||||
*
|
||||
* @param requestId 请求 ID
|
||||
* @param uploadId 内部上传 ID
|
||||
* @param handle 文件句柄
|
||||
* @return 上传记录
|
||||
*/
|
||||
private WorkflowApiUploadRecord storedRecord(
|
||||
String requestId,
|
||||
String uploadId,
|
||||
FileStorageWriteHandle handle) {
|
||||
WorkflowApiUploadRecord record =
|
||||
new WorkflowApiUploadRecord();
|
||||
record.setRequestId(requestId);
|
||||
record.setUploadId(uploadId);
|
||||
record.setRequestId(HTTP_REQUEST_ID);
|
||||
record.setStoredFiles(List.of(
|
||||
new WorkflowApiStoredFile(
|
||||
"/files/" + handle.getFilename(),
|
||||
|
||||
@@ -116,18 +116,34 @@ public class WorkflowApiUploadStoreTest {
|
||||
.contains("redis.call('del', KEYS[3])"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证旧版 Redis 记录仍可把历史 requestId 作为内部上传 ID 读取。
|
||||
*
|
||||
* @throws Exception JSON 反序列化失败
|
||||
*/
|
||||
@Test
|
||||
public void legacyRecordShouldResolveHistoricalUploadId()
|
||||
throws Exception {
|
||||
WorkflowApiUploadRecord record = new ObjectMapper().readValue(
|
||||
"{\"requestId\":\"legacy-upload-1\"}",
|
||||
WorkflowApiUploadRecord.class);
|
||||
|
||||
Assert.assertEquals("legacy-upload-1", record.getUploadId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建测试上传记录。
|
||||
*
|
||||
* @param requestId 上传请求 ID
|
||||
* @param uploadId 内部上传 ID
|
||||
* @param executeId 执行 ID
|
||||
* @return 上传记录
|
||||
*/
|
||||
private WorkflowApiUploadRecord record(
|
||||
String requestId,
|
||||
String uploadId,
|
||||
String executeId) {
|
||||
WorkflowApiUploadRecord record = new WorkflowApiUploadRecord();
|
||||
record.setRequestId(requestId);
|
||||
record.setUploadId(uploadId);
|
||||
record.setRequestId("http-request-1");
|
||||
record.setExecuteId(executeId);
|
||||
record.setCleanupAt(1_000L);
|
||||
return record;
|
||||
|
||||
@@ -18,13 +18,13 @@ import java.util.Optional;
|
||||
*/
|
||||
public class WorkflowApiUploadedFileReaderTest {
|
||||
|
||||
private static final String REQUEST_ID =
|
||||
private static final String UPLOAD_ID =
|
||||
"0123456789abcdef0123456789abcdef";
|
||||
private static final String FILENAME =
|
||||
"000-abcdefabcdefabcdefabcdefabcdefab.docx";
|
||||
private static final String FILE_URL =
|
||||
"http://127.0.0.1:39000/easyflow/attachment/"
|
||||
+ "workflow-api-upload/" + REQUEST_ID + "/" + FILENAME;
|
||||
+ "workflow-api-upload/" + UPLOAD_ID + "/" + FILENAME;
|
||||
|
||||
/**
|
||||
* 验证 URL、上传记录与恢复句柄完全匹配后按固定后端读取。
|
||||
@@ -40,7 +40,7 @@ public class WorkflowApiUploadedFileReaderTest {
|
||||
FileStorageWriteHandle handle = handle();
|
||||
WorkflowApiUploadRecord record = record(FILE_URL, handle);
|
||||
byte[] content = "document-content".getBytes(StandardCharsets.UTF_8);
|
||||
Mockito.when(uploadStore.find(REQUEST_ID)).thenReturn(Optional.of(record));
|
||||
Mockito.when(uploadStore.find(UPLOAD_ID)).thenReturn(Optional.of(record));
|
||||
Mockito.when(fileStorageService.readRecoverable(handle))
|
||||
.thenReturn(new ByteArrayInputStream(content));
|
||||
|
||||
@@ -81,7 +81,7 @@ public class WorkflowApiUploadedFileReaderTest {
|
||||
FileStorageService fileStorageService = Mockito.mock(FileStorageService.class);
|
||||
WorkflowApiUploadedFileReader reader =
|
||||
new WorkflowApiUploadedFileReader(uploadStore, fileStorageService);
|
||||
Mockito.when(uploadStore.find(REQUEST_ID)).thenReturn(Optional.empty());
|
||||
Mockito.when(uploadStore.find(UPLOAD_ID)).thenReturn(Optional.empty());
|
||||
|
||||
IOException exception = Assert.assertThrows(
|
||||
IOException.class,
|
||||
@@ -100,7 +100,7 @@ public class WorkflowApiUploadedFileReaderTest {
|
||||
FileStorageService fileStorageService = Mockito.mock(FileStorageService.class);
|
||||
WorkflowApiUploadedFileReader reader =
|
||||
new WorkflowApiUploadedFileReader(uploadStore, fileStorageService);
|
||||
Mockito.when(uploadStore.find(REQUEST_ID)).thenReturn(Optional.of(
|
||||
Mockito.when(uploadStore.find(UPLOAD_ID)).thenReturn(Optional.of(
|
||||
record(FILE_URL + "?different=true", handle())));
|
||||
|
||||
IOException exception = Assert.assertThrows(
|
||||
@@ -121,7 +121,7 @@ public class WorkflowApiUploadedFileReaderTest {
|
||||
"local",
|
||||
"",
|
||||
"/tmp/easyflow-test",
|
||||
"workflow-api-upload/" + REQUEST_ID,
|
||||
"workflow-api-upload/" + UPLOAD_ID,
|
||||
FILENAME);
|
||||
}
|
||||
|
||||
@@ -136,7 +136,8 @@ public class WorkflowApiUploadedFileReaderTest {
|
||||
String fileUrl,
|
||||
FileStorageWriteHandle handle) {
|
||||
WorkflowApiUploadRecord record = new WorkflowApiUploadRecord();
|
||||
record.setRequestId(REQUEST_ID);
|
||||
record.setUploadId(UPLOAD_ID);
|
||||
record.setRequestId("http-request-1");
|
||||
record.setStoredFiles(List.of(new WorkflowApiStoredFile(
|
||||
fileUrl,
|
||||
handle.encodeLocator())));
|
||||
|
||||
Reference in New Issue
Block a user