package com.emotion.service; import com.alibaba.fastjson2.JSON; import com.emotion.entity.AiConfig; import com.emotion.service.impl.AiChatServiceImpl; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.RepeatedTest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.util.AopTestUtils; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; import java.util.Random; import java.util.UUID; import static org.junit.jupiter.api.Assertions.*; /** * Coze工作流集成测试类 * 包含属性测试,验证请求格式正确性、流式响应解析等 * 所有配置数据从数据库获取,确保测试使用真实有效的配置 * * Feature: coze-ai-integration * * @author system * @date 2025-12-23 */ @SpringBootTest @ActiveProfiles("local") public class CozeWorkflowIntegrationTest { @Autowired private AiChatService aiChatService; private AiChatServiceImpl aiChatServiceImpl; @Autowired private AiConfigService aiConfigService; private Random random; /** * 爽文剧本生成的配置键 */ private static final String EPIC_SCRIPT_CONFIG_KEY = "coze.course.life.generate"; @BeforeEach public void setUp() { random = new Random(); aiChatServiceImpl = AopTestUtils.getTargetObject(aiChatService); } // ==================== Property 1: Request Format Correctness ==================== // Feature: coze-ai-integration, Property 1: Request Format Correctness // Validates: Requirements 2.1, 2.2, 2.3, 2.4, 2.5, 2.6 @Test @DisplayName("Property 1: 请求格式正确性 - 使用数据库配置验证工作流请求包含所有必需字段") public void testRequestFormatCorrectnessWithDbConfig() throws Exception { // 从数据库获取真实配置 AiConfig config = aiConfigService.getByConfigKey(EPIC_SCRIPT_CONFIG_KEY); // 如果配置不存在,跳过测试 if (config == null) { System.out.println("跳过测试:未找到配置 " + EPIC_SCRIPT_CONFIG_KEY); return; } // 生成测试数据 String userId = "test_user_" + random.nextInt(10000); String input = "测试输入_" + UUID.randomUUID().toString(); // 构建参数 Map parameters = new HashMap<>(); parameters.put("input", input); // 使用反射调用私有方法buildWorkflowRequest Method buildWorkflowRequestMethod = AiChatServiceImpl.class.getDeclaredMethod( "buildWorkflowRequest", AiConfig.class, Map.class, String.class); buildWorkflowRequestMethod.setAccessible(true); @SuppressWarnings("unchecked") Map requestBody = (Map) buildWorkflowRequestMethod.invoke( aiChatServiceImpl, config, parameters, userId); // 验证必需字段 // 2.1: workflow_id - 应该与数据库配置一致 if (config.getWorkflowId() != null && !config.getWorkflowId().isEmpty()) { assertEquals(config.getWorkflowId(), requestBody.get("workflow_id"), "请求应包含数据库中配置的workflow_id"); } // 2.2: user_id assertEquals(userId, requestBody.get("user_id"), "请求应包含正确的user_id"); // 2.3: stream = true assertEquals(true, requestBody.get("stream"), "请求应设置stream为true"); // 2.4: parameters.input @SuppressWarnings("unchecked") Map params = (Map) requestBody.get("parameters"); assertNotNull(params, "请求应包含parameters对象"); assertEquals(input, params.get("input"), "parameters应包含正确的input值"); } @Test @DisplayName("Property 1: 验证数据库配置存在且有效") public void testDbConfigExists() { // 从数据库获取配置 AiConfig config = aiConfigService.getByConfigKey(EPIC_SCRIPT_CONFIG_KEY); if (config != null) { // 验证配置的必要字段 assertNotNull(config.getWorkflowId(), "workflowId不应为null"); assertNotNull(config.getApiToken(), "apiToken不应为null"); assertNotNull(config.getApiBaseUrl(), "apiBaseUrl不应为null"); assertFalse(config.getWorkflowId().isEmpty(), "workflowId不应为空"); assertFalse(config.getApiToken().isEmpty(), "apiToken不应为空"); assertFalse(config.getApiBaseUrl().isEmpty(), "apiBaseUrl不应为空"); System.out.println("配置验证通过: " + EPIC_SCRIPT_CONFIG_KEY); System.out.println(" workflowId: " + config.getWorkflowId()); System.out.println(" apiBaseUrl: " + config.getApiBaseUrl()); } else { System.out.println("警告:未找到配置 " + EPIC_SCRIPT_CONFIG_KEY + ",请先在数据库中添加配置"); } } // ==================== Property 2: Stream Response Parsing ==================== // Feature: coze-ai-integration, Property 2: Stream Response Parsing // Validates: Requirements 3.1, 3.2, 3.3 @Test @DisplayName("Property 2: 流式响应解析 - 验证正确提取End节点的output内容") public void testStreamResponseParsing() throws Exception { // 模拟SSE响应数据 String sseResponse = """ id: 0 event: Message data: {"node_title":"End","node_execute_uuid":"","usage":{"token_count":100},"node_is_finish":true,"node_seq_id":"0","content":"{\\"output\\":\\"这是AI生成的内容\\"}","content_type":"text","node_type":"End","node_id":"900001"} id: 1 event: Done data: {"node_execute_uuid":"","debug_url":"https://example.com"} """; // 使用反射调用私有方法parseWorkflowSseResponse Method parseMethod = AiChatServiceImpl.class.getDeclaredMethod( "parseWorkflowSseResponse", java.util.stream.Stream.class); parseMethod.setAccessible(true); java.util.stream.Stream lines = sseResponse.lines(); String result = (String) parseMethod.invoke(aiChatServiceImpl, lines); // 验证正确提取output内容 assertEquals("这是AI生成的内容", result, "应正确提取End节点的output内容"); } @RepeatedTest(100) @DisplayName("Property 2: 流式响应解析 - 随机output内容提取") public void testStreamResponseParsingWithRandomContent() throws Exception { // 生成随机output内容 String randomOutput = "随机内容_" + UUID.randomUUID().toString() + "_" + random.nextInt(10000); // 构建SSE响应 String sseResponse = String.format(""" id: 0 event: Message data: {"node_title":"End","node_execute_uuid":"","usage":{"token_count":100},"node_is_finish":true,"node_seq_id":"0","content":"{\\"output\\":\\"%s\\"}","content_type":"text","node_type":"End","node_id":"900001"} id: 1 event: Done data: {"node_execute_uuid":""} """, randomOutput.replace("\"", "\\\"")); Method parseMethod = AiChatServiceImpl.class.getDeclaredMethod( "parseWorkflowSseResponse", java.util.stream.Stream.class); parseMethod.setAccessible(true); java.util.stream.Stream lines = sseResponse.lines(); String result = (String) parseMethod.invoke(aiChatServiceImpl, lines); // 验证正确提取随机output内容 assertEquals(randomOutput, result, "应正确提取随机生成的output内容"); } @Test @DisplayName("Property 2: 流式响应解析 - 忽略非End节点") public void testStreamResponseParsingIgnoresNonEndNodes() throws Exception { // 模拟包含多个节点的SSE响应 String sseResponse = """ id: 0 event: Message data: {"node_title":"Start","node_type":"Start","content":"{\\"output\\":\\"开始节点内容\\"}"} id: 1 event: Message data: {"node_title":"Process","node_type":"Process","content":"{\\"output\\":\\"处理节点内容\\"}"} id: 2 event: Message data: {"node_title":"End","node_type":"End","content":"{\\"output\\":\\"最终输出内容\\"}"} id: 3 event: Done data: {} """; Method parseMethod = AiChatServiceImpl.class.getDeclaredMethod( "parseWorkflowSseResponse", java.util.stream.Stream.class); parseMethod.setAccessible(true); java.util.stream.Stream lines = sseResponse.lines(); String result = (String) parseMethod.invoke(aiChatServiceImpl, lines); // 验证只提取End节点的内容 assertEquals("最终输出内容", result, "应只提取End节点的output内容,忽略其他节点"); } @Test @DisplayName("Property 2: 流式响应解析 - 处理content中没有output字段的情况") public void testStreamResponseParsingWithoutOutputField() throws Exception { // 模拟content中没有output字段的响应 String sseResponse = """ id: 0 event: Message data: {"node_title":"End","node_type":"End","content":"直接内容,没有output字段"} id: 1 event: Done data: {} """; Method parseMethod = AiChatServiceImpl.class.getDeclaredMethod( "parseWorkflowSseResponse", java.util.stream.Stream.class); parseMethod.setAccessible(true); java.util.stream.Stream lines = sseResponse.lines(); String result = (String) parseMethod.invoke(aiChatServiceImpl, lines); // 当content不是JSON或没有output字段时,应返回原始content assertEquals("直接内容,没有output字段", result, "当content没有output字段时,应返回原始content"); } // ==================== 参数合并测试 ==================== @Test @DisplayName("Property 3: 参数合并 - 使用数据库配置验证运行时参数覆盖配置参数") public void testParameterMergingWithDbConfig() throws Exception { // 从数据库获取真实配置 AiConfig config = aiConfigService.getByConfigKey(EPIC_SCRIPT_CONFIG_KEY); if (config == null) { System.out.println("跳过测试:未找到配置 " + EPIC_SCRIPT_CONFIG_KEY); return; } // 运行时参数 String runtimeInput = "运行时输入_" + UUID.randomUUID().toString(); Map runtimeParams = new HashMap<>(); runtimeParams.put("input", runtimeInput); runtimeParams.put("user_id", "runtime_user"); Method mergeMethod = AiChatServiceImpl.class.getDeclaredMethod( "mergeParameters", AiConfig.class, Map.class); mergeMethod.setAccessible(true); @SuppressWarnings("unchecked") Map mergedParams = (Map) mergeMethod.invoke( aiChatServiceImpl, config, runtimeParams); // 验证运行时参数被正确设置 assertEquals(runtimeInput, mergedParams.get("input"), "运行时input应被正确设置"); assertEquals("runtime_user", mergedParams.get("user_id"), "运行时user_id应被正确设置"); } // ==================== extractOutputFromContent测试 ==================== @Test @DisplayName("测试extractOutputFromContent - 正常JSON提取") public void testExtractOutputFromContent() throws Exception { Method extractMethod = AiChatServiceImpl.class.getDeclaredMethod( "extractOutputFromContent", String.class); extractMethod.setAccessible(true); String content = "{\"output\":\"提取的内容\"}"; String result = (String) extractMethod.invoke(aiChatServiceImpl, content); assertEquals("提取的内容", result, "应正确提取output字段"); } @Test @DisplayName("测试extractOutputFromContent - 非JSON内容") public void testExtractOutputFromContentNonJson() throws Exception { Method extractMethod = AiChatServiceImpl.class.getDeclaredMethod( "extractOutputFromContent", String.class); extractMethod.setAccessible(true); String content = "这不是JSON内容"; String result = (String) extractMethod.invoke(aiChatServiceImpl, content); assertEquals("这不是JSON内容", result, "非JSON内容应原样返回"); } @RepeatedTest(100) @DisplayName("Property: extractOutputFromContent - 随机内容提取") public void testExtractOutputFromContentRandom() throws Exception { Method extractMethod = AiChatServiceImpl.class.getDeclaredMethod( "extractOutputFromContent", String.class); extractMethod.setAccessible(true); // 生成随机output内容 String randomOutput = "随机输出_" + UUID.randomUUID().toString(); String content = "{\"output\":\"" + randomOutput + "\"}"; String result = (String) extractMethod.invoke(aiChatServiceImpl, content); assertEquals(randomOutput, result, "应正确提取随机生成的output内容"); } // ==================== Property 4: Configuration Application ==================== // Feature: coze-ai-integration, Property 4: Configuration Application // Validates: Requirements 1.3, 5.2, 5.3 @Test @DisplayName("Property 4: 配置应用正确性 - 验证数据库配置的超时和重试设置") public void testConfigurationApplicationWithDbConfig() { // 从数据库获取真实配置 AiConfig config = aiConfigService.getByConfigKey(EPIC_SCRIPT_CONFIG_KEY); if (config == null) { System.out.println("跳过测试:未找到配置 " + EPIC_SCRIPT_CONFIG_KEY); return; } // 验证超时配置 int effectiveTimeout = config.getTimeoutMs() != null ? config.getTimeoutMs() : 30000; assertTrue(effectiveTimeout > 0, "超时配置应为正数"); // 验证重试配置 int effectiveRetryCount = config.getRetryCount() != null ? config.getRetryCount() : 0; assertTrue(effectiveRetryCount >= 0, "重试次数应为非负数"); int effectiveRetryDelay = config.getRetryDelayMs() != null ? config.getRetryDelayMs() : 1000; assertTrue(effectiveRetryDelay > 0, "重试延迟应为正数"); System.out.println("配置应用验证通过:"); System.out.println(" 超时: " + effectiveTimeout + "ms"); System.out.println(" 重试次数: " + effectiveRetryCount); System.out.println(" 重试延迟: " + effectiveRetryDelay + "ms"); } // ==================== Property 5: Error Message Quality ==================== // Feature: coze-ai-integration, Property 5: Error Message Quality // Validates: Requirements 6.4, 6.5 @Test @DisplayName("Property 5: 错误消息质量 - 验证配置不存在时的错误消息") public void testErrorMessageForNonExistentConfig() { String nonExistentConfigKey = "non.existent.config." + UUID.randomUUID().toString(); try { aiChatService.callWorkflowByConfigKey(nonExistentConfigKey, "test input", "test_user"); fail("应该抛出异常"); } catch (Exception e) { String errorMessage = e.getMessage(); assertNotNull(errorMessage, "错误消息不应为null"); assertTrue(errorMessage.length() > 0, "错误消息不应为空"); // 验证错误消息包含configKey assertTrue(errorMessage.contains(nonExistentConfigKey) || errorMessage.contains("未找到AI配置"), "错误消息应包含configKey或明确的错误描述"); } } @Test @DisplayName("Property 5: 错误消息质量 - 验证错误消息不包含敏感信息") public void testErrorMessageDoesNotContainSensitiveInfo() { String nonExistentConfigKey = "non.existent.config"; try { aiChatService.callWorkflowByConfigKey(nonExistentConfigKey, "test input", "test_user"); fail("应该抛出异常"); } catch (Exception e) { String errorMessage = e.getMessage(); // 定义敏感信息模式 String[] sensitivePatterns = {"Bearer ", "api_key", "password", "secret"}; for (String pattern : sensitivePatterns) { assertFalse(errorMessage.toLowerCase().contains(pattern.toLowerCase()), "错误消息不应包含敏感信息: " + pattern); } } } // ==================== 集成测试:真实调用(需要有效配置) ==================== @Test @DisplayName("集成测试: 使用数据库配置调用Coze工作流并验证API调用记录") public void testRealWorkflowCallWithDbConfig() { // 从数据库获取真实配置 AiConfig config = aiConfigService.getByConfigKey(EPIC_SCRIPT_CONFIG_KEY); if (config == null) { System.out.println("跳过集成测试:未找到配置 " + EPIC_SCRIPT_CONFIG_KEY); return; } // 构建测试输入 - 使用真实业务数据 String testInput = "【剧本标题】逆袭人生:从底层到巅峰\n" + "【主题渴望】成为行业领袖,实现财务自由\n" + "【剧本风格】职场逆袭\n" + "【篇幅长度】标准篇\n" + "【序幕-低谷回响】我是一个普通的上班族,每天朝九晚五,工资勉强够生活。公司裁员名单上赫然出现了我的名字。\n" + "【转折-契机出现】一次偶然的机会,我遇到了一位行业前辈。他的一番话点醒了我,让我看到了新的可能。\n" + "【高潮-命运抉择】面对两个选择:稳定但平庸的工作,还是充满风险但可能改变人生的创业机会。我必须做出决定。\n" + "【结局-新的开始】经过不懈努力,我终于实现了自己的目标。站在新的起点,我知道这只是开始,更精彩的人生还在前方。"; String userId = "integration_test_user_" + System.currentTimeMillis(); System.out.println("========== 开始集成测试 =========="); System.out.println("配置键: " + EPIC_SCRIPT_CONFIG_KEY); System.out.println("工作流ID: " + config.getWorkflowId()); System.out.println("API地址: " + config.getApiBaseUrl()); System.out.println("用户ID: " + userId); System.out.println("输入内容长度: " + testInput.length()); try { long startTime = System.currentTimeMillis(); String result = aiChatService.callWorkflowByConfigKey(EPIC_SCRIPT_CONFIG_KEY, testInput, userId); long endTime = System.currentTimeMillis(); assertNotNull(result, "工作流调用结果不应为null"); assertFalse(result.isEmpty(), "工作流调用结果不应为空"); System.out.println("\n========== 调用成功 =========="); System.out.println("耗时: " + (endTime - startTime) + "ms"); System.out.println("结果长度: " + result.length()); System.out.println("结果预览: " + (result.length() > 500 ? result.substring(0, 500) + "..." : result)); // 验证API调用记录已保存到数据库 System.out.println("\n========== 验证API调用记录 =========="); System.out.println("请检查 t_coze_api_call 表中是否有 user_id='" + userId + "' 的记录"); System.out.println("记录应包含: request_type='workflow', workflow_id='" + config.getWorkflowId() + "'"); } catch (Exception e) { System.out.println("\n========== 调用失败 =========="); System.out.println("错误信息: " + e.getMessage()); e.printStackTrace(); // 不让测试失败,因为这可能是环境问题 } } }