feat: 完善工作流 Public API 调用能力

- 支持 JSON 文件 URL 简写与 Multipart 单请求文件上传

- 完善执行拓扑、枚举状态、节点名称、恢复校验和安全错误响应

- 增加临时上传生命周期清理并升级 MinIO SDK

- 重构工作流接口调用说明弹窗的扁平响应式布局
This commit is contained in:
2026-08-09 21:27:30 +08:00
parent 0d14f1c165
commit 54d85ae460
61 changed files with 8131 additions and 161 deletions

View File

@@ -0,0 +1,191 @@
package tech.easyflow.publicapi.controller;
import com.easyagents.flow.core.chain.ChainStatus;
import com.easyagents.flow.core.chain.NodeStatus;
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.test.util.ReflectionTestUtils;
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
import tech.easyflow.ai.easyagentsflow.service.TinyFlowService;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.entity.WorkflowExecResult;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.ai.service.WorkflowApiPermissionService;
import tech.easyflow.ai.service.WorkflowExecResultService;
import tech.easyflow.ai.service.WorkflowService;
import tech.easyflow.ai.utils.WorkFlowUtil;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.publicapi.dto.PublicWorkflowChainStatus;
import tech.easyflow.publicapi.dto.PublicWorkflowExecutionStatus;
import tech.easyflow.publicapi.service.PublicWorkflowStatusSanitizer;
import tech.easyflow.system.entity.SysApiKey;
import java.math.BigInteger;
import java.util.List;
import java.util.Map;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* {@link PublicWorkflowController} 状态查询与恢复行为测试。
*/
public class PublicWorkflowControllerBehaviorTest {
private static final String EXECUTE_ID = "execute-1";
private PublicWorkflowController controller;
private ChainExecutor chainExecutor;
private TinyFlowService tinyFlowService;
private HttpServletRequest request;
/**
* 创建通过 API Key 执行归属校验的控制器测试夹具。
*/
@Before
public void setUp() {
controller = new PublicWorkflowController();
chainExecutor = Mockito.mock(ChainExecutor.class);
tinyFlowService = Mockito.mock(TinyFlowService.class);
WorkflowApiPermissionService permissionService =
Mockito.mock(WorkflowApiPermissionService.class);
WorkflowExecResultService execResultService =
Mockito.mock(WorkflowExecResultService.class);
WorkflowService workflowService =
Mockito.mock(WorkflowService.class);
request = Mockito.mock(HttpServletRequest.class);
SysApiKey apiKey = new SysApiKey();
apiKey.setId(BigInteger.TEN);
when(request.getHeader("ApiKey")).thenReturn("api-key");
when(request.getRequestURI()).thenReturn(
"/public-api/workflow/getChainStatus");
when(permissionService.assertWorkflowApi(any(), anyString()))
.thenReturn(apiKey);
when(execResultService.getByExecKey(EXECUTE_ID))
.thenReturn(executionRecord());
when(workflowService.getById(BigInteger.ONE))
.thenReturn(publishedWorkflow());
ReflectionTestUtils.setField(
controller,
"chainExecutor",
chainExecutor);
ReflectionTestUtils.setField(
controller,
"tinyFlowService",
tinyFlowService);
ReflectionTestUtils.setField(
controller,
"workflowApiPermissionService",
permissionService);
ReflectionTestUtils.setField(
controller,
"workflowExecResultService",
execResultService);
ReflectionTestUtils.setField(
controller,
"workflowService",
workflowService);
ReflectionTestUtils.setField(
controller,
"publicWorkflowStatusSanitizer",
new PublicWorkflowStatusSanitizer());
}
/**
* 验证非暂停状态恢复返回稳定冲突错误,且不会调用旧的无条件恢复入口。
*/
@Test
public void resumeShouldRejectNonSuspendedExecution() {
when(request.getRequestURI()).thenReturn(
"/public-api/workflow/resume");
when(chainExecutor.resumeAsyncIfSuspended(
EXECUTE_ID,
Map.of("approved", true)))
.thenReturn(false);
try {
controller.resume(
EXECUTE_ID,
Map.of("approved", true),
request);
Assert.fail("非暂停状态必须拒绝恢复");
} catch (BusinessException exception) {
Assert.assertEquals(409, exception.getHttpStatus());
Assert.assertEquals(40901, exception.getErrorCode());
}
verify(chainExecutor).resumeAsyncIfSuspended(
EXECUTE_ID,
Map.of("approved", true));
}
/**
* 验证状态查询使用定义快照补齐节点名称并返回可读枚举。
*/
@Test
public void statusShouldEnrichNodeNameAndReadableEnum() {
ChainInfo chainInfo = new ChainInfo();
chainInfo.setExecuteId(EXECUTE_ID);
chainInfo.setStatus(ChainStatus.RUNNING.getValue());
NodeInfo node = new NodeInfo();
node.setNodeId("node-1");
node.setNodeName("文档解析");
node.setStatus(NodeStatus.RUNNING.getValue());
chainInfo.setNodes(Map.of("node-1", node));
when(tinyFlowService.getChainStatus(
EXECUTE_ID,
List.of(node)))
.thenReturn(chainInfo);
Result<PublicWorkflowChainStatus> result =
controller.getChainStatus(
EXECUTE_ID,
List.of(node),
request);
Assert.assertEquals(
PublicWorkflowExecutionStatus.RUNNING,
result.getData().status());
Assert.assertFalse(result.getData().terminal());
Assert.assertEquals(
"文档解析",
result.getData().nodes().get("node-1").nodeName());
}
/**
* 创建属于当前 API Key 的执行记录。
*
* @return 执行记录
*/
private WorkflowExecResult executionRecord() {
WorkflowExecResult result = new WorkflowExecResult();
result.setExecKey(EXECUTE_ID);
result.setWorkflowId(BigInteger.ONE);
result.setCreatedKey(WorkFlowUtil.API_KEY);
result.setCreatedBy(BigInteger.TEN.toString());
return result;
}
/**
* 创建可恢复校验使用的已发布工作流。
*
* @return 已发布工作流
*/
private Workflow publishedWorkflow() {
Workflow workflow = new Workflow();
workflow.setId(BigInteger.ONE);
workflow.setPublishStatus(PublishStatus.PUBLISHED.getCode());
workflow.setPublishedSnapshotJson(Map.of("nodes", List.of()));
return workflow;
}
}

View File

@@ -1,9 +1,23 @@
package tech.easyflow.publicapi.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.publicapi.dto.PublicWorkflowChainStatus;
import tech.easyflow.publicapi.dto.PublicWorkflowExecutionStatus;
import tech.easyflow.publicapi.dto.PublicWorkflowInfo;
import tech.easyflow.publicapi.dto.PublicWorkflowRunResult;
import tech.easyflow.publicapi.dto.PublicWorkflowTopology;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* {@link PublicWorkflowController} HTTP 响应契约测试。
@@ -23,4 +37,139 @@ public class PublicWorkflowControllerContractTest {
requestMapping.produces()
);
}
/**
* 验证 multipart 调用复用 runAsync 路径并明确声明媒体类型。
*/
@Test
public void shouldExposeMultipartRunAsyncOnCompatiblePath() {
Method multipartMethod = Arrays.stream(
PublicWorkflowController.class.getDeclaredMethods())
.filter(method -> "runAsyncMultipart".equals(
method.getName()))
.findFirst()
.orElseThrow();
PostMapping postMapping =
multipartMethod.getAnnotation(PostMapping.class);
Assert.assertNotNull(postMapping);
Assert.assertArrayEquals(
new String[]{"/runAsync"},
postMapping.value());
Assert.assertArrayEquals(
new String[]{MediaType.MULTIPART_FORM_DATA_VALUE},
postMapping.consumes());
}
/**
* 验证 JSON 调用显式声明媒体类型,避免与 Multipart 路由混淆。
*/
@Test
public void shouldExposeJsonRunAsyncWithExplicitMediaType() {
Method jsonMethod = Arrays.stream(
PublicWorkflowController.class.getDeclaredMethods())
.filter(method -> "runAsync".equals(method.getName()))
.findFirst()
.orElseThrow();
PostMapping postMapping =
jsonMethod.getAnnotation(PostMapping.class);
Assert.assertNotNull(postMapping);
Assert.assertArrayEquals(
new String[]{MediaType.APPLICATION_JSON_VALUE},
postMapping.consumes());
}
/**
* 验证富响应继续把执行 ID 保存在 data 字符串中。
*
* @throws Exception JSON 序列化失败
*/
@Test
public void runResultShouldKeepLegacyExecuteIdData()
throws Exception {
PublicWorkflowTopology topology =
new PublicWorkflowTopology(
"1",
null,
"测试工作流",
null,
1,
null,
List.of(),
List.of(),
List.of(),
List.of(),
false,
List.of());
PublicWorkflowRunResult result =
PublicWorkflowRunResult.success(
"execution-1",
topology);
Assert.assertEquals("execution-1", result.getData());
Assert.assertSame(topology, result.getWorkflow());
Assert.assertEquals(0, result.getErrorCode());
String json = new ObjectMapper().writeValueAsString(result);
Assert.assertTrue(json.contains("\"data\":\"execution-1\""));
Assert.assertTrue(json.contains("\"workflow\""));
}
/**
* 验证详情接口 DTO 只公开调用所需的基础字段。
*
* @throws Exception JSON 序列化失败
*/
@Test
public void workflowInfoShouldExcludeInternalFields()
throws Exception {
Workflow workflow = new Workflow();
workflow.setId(new BigInteger("9007199254740993"));
workflow.setAlias("document-parser");
workflow.setTitle("文档解析");
workflow.setContent("internal-content");
workflow.setTenantId(BigInteger.TEN);
workflow.setDeptId(BigInteger.ONE);
workflow.setPublishedSnapshotJson(
Map.of("secret", "snapshot"));
String json = new ObjectMapper().writeValueAsString(
PublicWorkflowInfo.from(workflow));
Assert.assertTrue(json.contains(
"\"id\":\"9007199254740993\""));
Assert.assertTrue(json.contains(
"\"alias\":\"document-parser\""));
Assert.assertFalse(json.contains("content"));
Assert.assertFalse(json.contains("tenantId"));
Assert.assertFalse(json.contains("deptId"));
Assert.assertFalse(json.contains("publishedSnapshotJson"));
Assert.assertFalse(json.contains("secret"));
}
/**
* 验证链路状态以小写可读枚举返回,不再公开内部数值。
*
* @throws Exception JSON 序列化失败
*/
@Test
public void chainStatusShouldSerializeReadableEnum()
throws Exception {
PublicWorkflowChainStatus status =
new PublicWorkflowChainStatus(
"execute-1",
PublicWorkflowExecutionStatus.DONE,
true,
null,
Map.of("output", "ok"),
Map.of(),
null);
String json = new ObjectMapper().writeValueAsString(status);
Assert.assertTrue(json.contains("\"status\":\"done\""));
Assert.assertTrue(json.contains("\"terminal\":true"));
Assert.assertFalse(json.contains("\"status\":20"));
}
}

View File

@@ -0,0 +1,445 @@
package tech.easyflow.publicapi.controller;
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
import tech.easyflow.ai.easyagentsflow.service.WorkflowRunningParameterResolver;
import tech.easyflow.ai.easyagentsflow.upload.WorkflowApiPreparedUpload;
import tech.easyflow.ai.easyagentsflow.upload.WorkflowApiUploadLifecycleService;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.ai.service.WorkflowApiPermissionService;
import tech.easyflow.ai.service.WorkflowService;
import tech.easyflow.common.web.error.GlobalErrorResolver;
import tech.easyflow.publicapi.dto.PublicWorkflowTopology;
import tech.easyflow.publicapi.interceptor.PublicApiRequestContextFilter;
import tech.easyflow.publicapi.service.PublicWorkflowTopologyService;
import tech.easyflow.publicapi.service.WorkflowApiMultipartParameterMapper;
import tech.easyflow.system.entity.SysApiKey;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* {@link PublicWorkflowController} 真实 MVC 路由与错误契约测试。
*/
public class PublicWorkflowControllerRoutingTest {
private static final String RUN_PATH =
"/public-api/workflow/runAsync";
private MockMvc mockMvc;
private WorkflowService workflowService;
private WorkflowRunningParameterResolver parameterResolver;
private WorkflowApiMultipartParameterMapper multipartMapper;
private WorkflowApiUploadLifecycleService uploadLifecycleService;
/**
* 创建控制器和全部最小依赖。
*/
@Before
public void setUp() {
PublicWorkflowController controller =
new PublicWorkflowController();
workflowService = Mockito.mock(WorkflowService.class);
WorkflowCheckService workflowCheckService =
Mockito.mock(WorkflowCheckService.class);
WorkflowApiPermissionService permissionService =
Mockito.mock(WorkflowApiPermissionService.class);
PublicWorkflowTopologyService topologyService =
Mockito.mock(PublicWorkflowTopologyService.class);
ChainExecutor chainExecutor =
Mockito.mock(ChainExecutor.class);
parameterResolver = Mockito.mock(
WorkflowRunningParameterResolver.class);
multipartMapper = Mockito.mock(
WorkflowApiMultipartParameterMapper.class);
uploadLifecycleService = Mockito.mock(
WorkflowApiUploadLifecycleService.class);
Workflow workflow = publishedWorkflow();
SysApiKey apiKey = new SysApiKey();
apiKey.setId(BigInteger.TEN);
when(permissionService.assertWorkflowApi(any(), anyString()))
.thenReturn(apiKey);
when(workflowService.getPublishedById(BigInteger.ONE))
.thenReturn(workflow);
when(workflowService.getPublishedDetail("document-parser"))
.thenReturn(workflow);
when(topologyService.resolve(workflow))
.thenReturn(topology());
when(parameterResolver.normalizeRuntimeVariables(
eq("{}"),
anyMap()))
.thenAnswer(invocation -> invocation.getArgument(1));
when(chainExecutor.executeAsync(
anyString(),
anyMap(),
Mockito.<Consumer<String>>any()))
.thenAnswer(invocation -> {
Consumer<String> beforeStart =
invocation.getArgument(2);
if (beforeStart != null) {
beforeStart.accept("execute-1");
}
return "execute-1";
});
when(multipartMapper.map(any()))
.thenReturn(Map.of("file", List.of()));
when(uploadLifecycleService.prepare(
eq("{}"),
anyMap(),
anyMap()))
.thenReturn(new WorkflowApiPreparedUpload(
"upload-1",
Map.of()));
ReflectionTestUtils.setField(
controller,
"workflowService",
workflowService);
ReflectionTestUtils.setField(
controller,
"workflowCheckService",
workflowCheckService);
ReflectionTestUtils.setField(
controller,
"workflowApiPermissionService",
permissionService);
ReflectionTestUtils.setField(
controller,
"publicWorkflowTopologyService",
topologyService);
ReflectionTestUtils.setField(
controller,
"chainExecutor",
chainExecutor);
ReflectionTestUtils.setField(
controller,
"workflowRunningParameterResolver",
parameterResolver);
ReflectionTestUtils.setField(
controller,
"workflowApiMultipartParameterMapper",
multipartMapper);
ReflectionTestUtils.setField(
controller,
"workflowApiUploadLifecycleService",
uploadLifecycleService);
LocalValidatorFactoryBean validator =
new LocalValidatorFactoryBean();
validator.afterPropertiesSet();
mockMvc = MockMvcBuilders.standaloneSetup(controller)
.setValidator(validator)
.setHandlerExceptionResolvers(
new GlobalErrorResolver())
.addFilters(new PublicApiRequestContextFilter())
.build();
}
/**
* 验证 application/json 只进入 JSON 调用链。
*
* @throws Exception 请求执行失败
*/
@Test
public void jsonRequestShouldUseJsonHandler() throws Exception {
mockMvc.perform(post(RUN_PATH)
.header("ApiKey", "key")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"id\":1,\"variables\":{}}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data").value("execute-1"));
verify(parameterResolver).normalizeRuntimeVariables(
eq("{}"),
anyMap());
verify(uploadLifecycleService, never()).prepare(
anyString(),
anyMap(),
anyMap());
}
/**
* 验证 multipart/form-data 只进入文件直传调用链。
*
* @throws Exception 请求执行失败
*/
@Test
public void multipartRequestShouldUseMultipartHandler()
throws Exception {
MockMultipartFile metadata = new MockMultipartFile(
"metadata",
"metadata.json",
MediaType.APPLICATION_JSON_VALUE,
"{\"id\":1,\"variables\":{}}"
.getBytes(StandardCharsets.UTF_8));
MockMultipartFile file = new MockMultipartFile(
"files.file",
"report.pdf",
MediaType.APPLICATION_PDF_VALUE,
"pdf".getBytes(StandardCharsets.UTF_8));
mockMvc.perform(multipart(RUN_PATH)
.file(metadata)
.file(file)
.header("ApiKey", "key"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data").value("execute-1"));
verify(multipartMapper).map(any());
verify(uploadLifecycleService).prepare(
eq("{}"),
anyMap(),
anyMap());
}
/**
* 验证错误顶层媒体类型返回 41501。
*
* @throws Exception 请求执行失败
*/
@Test
public void unsupportedContentTypeShouldExplainExpectedTypes()
throws Exception {
mockMvc.perform(post(RUN_PATH)
.header("ApiKey", "key")
.header("X-Request-Id", "request-415")
.contentType(MediaType.TEXT_PLAIN)
.content("{}"))
.andExpect(status().isUnsupportedMediaType())
.andExpect(jsonPath("$.errorCode").value(41501))
.andExpect(jsonPath("$.data.field")
.value("Content-Type"))
.andExpect(jsonPath("$.data.requestId")
.value("request-415"))
.andExpect(header().string(
"X-Request-Id",
"request-415"));
}
/**
* 验证缺少顶层媒体类型时返回 41501。
*
* @throws Exception 请求执行失败
*/
@Test
public void missingContentTypeShouldExplainExpectedTypes()
throws Exception {
mockMvc.perform(post(RUN_PATH)
.header("ApiKey", "key")
.content("{}"))
.andExpect(status().isUnsupportedMediaType())
.andExpect(jsonPath("$.errorCode").value(41501))
.andExpect(jsonPath("$.data.expected.length()")
.value(2));
}
/**
* 验证无效 JSON 返回 40011不再误报 ID 为空。
*
* @throws Exception 请求执行失败
*/
@Test
public void malformedJsonShouldReturnBodyMismatchError()
throws Exception {
mockMvc.perform(post(RUN_PATH)
.header("ApiKey", "key")
.contentType(MediaType.APPLICATION_JSON)
.content("not-json"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errorCode").value(40011));
}
/**
* 验证 Multipart 缺少 metadata 时返回 40013。
*
* @throws Exception 请求执行失败
*/
@Test
public void missingMetadataShouldReturnSpecificError()
throws Exception {
MockMultipartFile file = new MockMultipartFile(
"files.file",
"report.pdf",
MediaType.APPLICATION_PDF_VALUE,
"pdf".getBytes(StandardCharsets.UTF_8));
mockMvc.perform(multipart(RUN_PATH)
.file(file)
.header("ApiKey", "key"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errorCode").value(40013));
}
/**
* 验证 metadata Part 媒体类型错误时返回 41502。
*
* @throws Exception 请求执行失败
*/
@Test
public void metadataWithWrongContentTypeShouldReturnSpecificError()
throws Exception {
MockMultipartFile metadata = new MockMultipartFile(
"metadata",
"metadata.txt",
MediaType.TEXT_PLAIN_VALUE,
"{\"id\":1,\"variables\":{}}"
.getBytes(StandardCharsets.UTF_8));
mockMvc.perform(multipart(RUN_PATH)
.file(metadata)
.header("ApiKey", "key"))
.andExpect(status().isUnsupportedMediaType())
.andExpect(jsonPath("$.errorCode").value(41502))
.andExpect(jsonPath("$.data.expected[0]")
.value(MediaType.APPLICATION_JSON_VALUE));
}
/**
* 验证 Multipart metadata 缺少工作流 ID 时返回 40015。
*
* @throws Exception 请求执行失败
*/
@Test
public void metadataWithoutIdShouldReturnSpecificError()
throws Exception {
MockMultipartFile metadata = new MockMultipartFile(
"metadata",
"metadata.json",
MediaType.APPLICATION_JSON_VALUE,
"{\"variables\":{}}"
.getBytes(StandardCharsets.UTF_8));
mockMvc.perform(multipart(RUN_PATH)
.file(metadata)
.header("ApiKey", "key"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errorCode").value(40015))
.andExpect(jsonPath("$.data.field").value("id"));
}
/**
* 验证 metadata Part 内容不是合法 JSON 时返回 40014。
*
* @throws Exception 请求执行失败
*/
@Test
public void malformedMetadataShouldReturnSpecificError()
throws Exception {
MockMultipartFile metadata = new MockMultipartFile(
"metadata",
"metadata.json",
MediaType.APPLICATION_JSON_VALUE,
"not-json".getBytes(StandardCharsets.UTF_8));
mockMvc.perform(multipart(RUN_PATH)
.file(metadata)
.header("ApiKey", "key"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errorCode").value(40014));
}
/**
* 验证详情接口只返回安全基础字段。
*
* @throws Exception 请求执行失败
*/
@Test
public void workflowDetailShouldReturnSafeInfo()
throws Exception {
mockMvc.perform(get(
"/public-api/workflow/getByIdOrAlias")
.header("ApiKey", "key")
.param("key", "document-parser"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.id").value("1"))
.andExpect(jsonPath("$.data.content").doesNotExist())
.andExpect(jsonPath("$.data.tenantId").doesNotExist())
.andExpect(jsonPath("$.data.deptId").doesNotExist())
.andExpect(jsonPath("$.data.publishedSnapshotJson")
.doesNotExist());
}
/**
* 验证运行参数解析失败时返回真实 HTTP 500 和稳定错误码。
*
* @throws Exception 请求执行失败
*/
@Test
public void invalidRunningParametersShouldReturnServerError()
throws Exception {
when(parameterResolver.buildRunningParametersView(any()))
.thenReturn(null);
mockMvc.perform(get(
"/public-api/workflow/getRunningParameters")
.header("ApiKey", "key")
.param("id", "1"))
.andExpect(status().isInternalServerError())
.andExpect(jsonPath("$.errorCode").value(50001));
}
/**
* 创建已发布工作流桩。
*
* @return 已发布工作流
*/
private Workflow publishedWorkflow() {
Workflow workflow = new Workflow();
workflow.setId(BigInteger.ONE);
workflow.setAlias("document-parser");
workflow.setTitle("文档解析");
workflow.setContent("{}");
workflow.setPublishStatus(PublishStatus.PUBLISHED.getCode());
workflow.setPublishedSnapshotJson(Map.of("nodes", List.of()));
return workflow;
}
/**
* 创建最小公开拓扑。
*
* @return 公开拓扑
*/
private PublicWorkflowTopology topology() {
return new PublicWorkflowTopology(
"1",
null,
"测试工作流",
null,
1,
null,
List.of(),
List.of(),
List.of(),
List.of(),
false,
List.of());
}
}