238 lines
10 KiB
Java
Raw Normal View History

2026-07-31 16:29:10 +08:00
package com.company.agentfoundation.assistant.api;
import com.company.agentfoundation.integration.coze.service.CozeWorkflowService;
import com.company.agentfoundation.semantic.SemanticDispatchResult;
import com.company.agentfoundation.semantic.SemanticDispatchService;
import com.company.agentfoundation.semantic.SemanticReplyAttachment;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import java.util.List;
2026-08-03 09:27:19 +08:00
import java.util.concurrent.Executor;
2026-07-31 16:29:10 +08:00
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
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.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
class AssistantApiControllerTest {
@Test
void generalApiCallsOriginalCozeWorkflowWithoutAuthorizationHeader() throws Exception {
CozeWorkflowService coze = mock(CozeWorkflowService.class);
when(coze.chatDefault("测试表")).thenReturn("扣子回答");
MockMvc mvc = mvc(coze, mock(SemanticDispatchService.class), new AssistantFileStore());
mvc.perform(post("/api/assistant/general/query")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"query\":\"测试表\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.content").value("扣子回答"));
verify(coze).chatDefault("测试表");
}
2026-08-03 09:27:19 +08:00
@Test
void generalApiAcceptsCustomerCreationAsynchronously() throws Exception {
CozeWorkflowService coze = mock(CozeWorkflowService.class);
CapturingExecutor executor = new CapturingExecutor();
MockMvc mvc = mvc(coze, mock(SemanticDispatchService.class), new AssistantFileStore(), executor);
mvc.perform(post("/api/assistant/general/query")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"query\":\"新增客户 名称 张三 电话 13632363654\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.content").value(org.hamcrest.Matchers.containsString("已受理")));
org.mockito.Mockito.verifyNoInteractions(coze);
executor.runCaptured();
verify(coze).chatDefault("新增客户 名称 张三 电话 13632363654");
}
2026-07-31 16:29:10 +08:00
@Test
void sportsVenueApiRunsSemanticRoutingAndReturnsDownloadableExcel() throws Exception {
SemanticDispatchService semantic = mock(SemanticDispatchService.class);
byte[] excel = {80, 75, 1, 2};
when(semantic.dispatchWithAttachments("YiNianYiQingJing", "查看今日EC日报"))
.thenReturn(new SemanticDispatchResult(
"EC日报内容",
List.of(SemanticReplyAttachment.excel("EC日报.xlsx", excel))));
MockMvc mvc = mvc(mock(CozeWorkflowService.class), semantic, new AssistantFileStore());
MvcResult result = mvc.perform(post("/api/assistant/sports-venue/query")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"content":"查看今日EC日报","wecomUserId":"YiNianYiQingJing"}
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.content").value(org.hamcrest.Matchers.containsString("EC日报内容")))
.andExpect(jsonPath("$.content").value(org.hamcrest.Matchers.containsString("EC日报.xlsx")))
.andExpect(jsonPath("$.attachments[0].name").value("EC日报.xlsx"))
.andReturn();
String response = result.getResponse().getContentAsString();
String url = new com.fasterxml.jackson.databind.ObjectMapper().readTree(response)
.path("attachments").get(0).path("downloadUrl").asText();
String path = java.net.URI.create(url).getPath();
MvcResult download = mvc.perform(get(path))
.andExpect(status().isOk())
.andReturn();
assertThat(download.getResponse().getContentAsByteArray()).isEqualTo(excel);
}
@Test
2026-07-31 16:42:15 +08:00
void sportsVenueApiReadsUserIdFromWecomContextHeader() throws Exception {
SemanticDispatchService semantic = mock(SemanticDispatchService.class);
when(semantic.dispatchWithAttachments("YiNianYiQingJing", "查看今日客资日报"))
.thenReturn(new SemanticDispatchResult("客资日报", List.of()));
MockMvc mvc = mvc(
mock(CozeWorkflowService.class),
semantic,
new AssistantFileStore());
mvc.perform(post("/api/assistant/sports-venue/query")
.header("X-WeCom-UserId", "YiNianYiQingJing")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"query\":\"查看今日客资日报\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true));
verify(semantic).dispatchWithAttachments("YiNianYiQingJing", "查看今日客资日报");
}
2026-07-31 16:48:49 +08:00
@Test
void sportsVenueApiSupportsWecomAppendedToolNamePath() throws Exception {
SemanticDispatchService semantic = mock(SemanticDispatchService.class);
when(semantic.dispatchWithAttachments("YiNianYiQingJing", "客资日报"))
.thenReturn(new SemanticDispatchResult("客资日报结果", List.of()));
MockMvc mvc = mvc(
mock(CozeWorkflowService.class),
semantic,
new AssistantFileStore());
mvc.perform(post("/api/assistant/sports-venue/query/sports-venue")
.header("userid", "YiNianYiQingJing")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"query\":\"客资日报\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.content").value("客资日报结果"));
}
@Test
void sportsVenueApiSupportsWecomTrailingSlashPath() throws Exception {
SemanticDispatchService semantic = mock(SemanticDispatchService.class);
when(semantic.dispatchWithAttachments("YiNianYiQingJing", "客资日报"))
.thenReturn(new SemanticDispatchResult("客资日报结果", List.of()));
MockMvc mvc = mvc(
mock(CozeWorkflowService.class),
semantic,
new AssistantFileStore());
mvc.perform(post("/api/assistant/sports-venue/query/")
.header("userid", "YiNianYiQingJing")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"query\":\"客资日报\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true));
2026-07-31 16:59:16 +08:00
}
@Test
void sportsVenueApiRecognizesPrefixedWecomUserIdHeader() throws Exception {
SemanticDispatchService semantic = mock(SemanticDispatchService.class);
when(semantic.dispatchWithAttachments("YiNianYiQingJing", "客资日报"))
.thenReturn(new SemanticDispatchResult("客资日报结果", List.of()));
MockMvc mvc = mvc(
mock(CozeWorkflowService.class),
semantic,
new AssistantFileStore());
mvc.perform(post("/api/assistant/sports-venue/query/")
.header("X-WeCom-From-UserId", "YiNianYiQingJing")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"query\":\"客资日报\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true));
2026-07-31 16:48:49 +08:00
}
2026-07-31 16:42:15 +08:00
@Test
void sportsVenueApiRejectsRequestWhenWecomContextHasNoUserId() throws Exception {
2026-07-31 16:29:10 +08:00
MockMvc mvc = mvc(
mock(CozeWorkflowService.class),
mock(SemanticDispatchService.class),
new AssistantFileStore());
mvc.perform(post("/api/assistant/sports-venue/query")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"query\":\"查看今日客资日报\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(false))
2026-07-31 16:42:15 +08:00
.andExpect(jsonPath("$.content").value(org.hamcrest.Matchers.containsString("UserID")));
2026-07-31 16:29:10 +08:00
}
2026-07-31 17:59:10 +08:00
@Test
void sportsVenueApiRemovesUnsupportedWecomFontTags() throws Exception {
SemanticDispatchService semantic = mock(SemanticDispatchService.class);
when(semantic.dispatchWithAttachments("tester", "daily report"))
.thenReturn(new SemanticDispatchResult(
"new: <font color=\"info\">35</font>\n"
+ "warning: <font color=\"warning\">low rate</font>",
List.of()));
MockMvc mvc = mvc(mock(CozeWorkflowService.class), semantic, new AssistantFileStore());
mvc.perform(post("/api/assistant/sports-venue/query")
.header("X-WeCom-UserId", "tester")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"query\":\"daily report\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.content").value("new: 35\nwarning: low rate"));
}
2026-07-31 16:29:10 +08:00
private MockMvc mvc(
CozeWorkflowService coze,
SemanticDispatchService semantic,
AssistantFileStore files
) {
2026-08-03 09:27:19 +08:00
return mvc(coze, semantic, files, Runnable::run);
}
private MockMvc mvc(
CozeWorkflowService coze,
SemanticDispatchService semantic,
AssistantFileStore files,
Executor executor
) {
return MockMvcBuilders.standaloneSetup(
new AssistantApiController(coze, semantic, files, executor))
2026-07-31 16:29:10 +08:00
.setMessageConverters(
new MappingJackson2HttpMessageConverter(),
new ByteArrayHttpMessageConverter())
.build();
}
2026-08-03 09:27:19 +08:00
private static final class CapturingExecutor implements Executor {
private Runnable captured;
@Override
public void execute(Runnable command) {
captured = command;
}
void runCaptured() {
if (captured == null) throw new AssertionError("No asynchronous task was submitted");
captured.run();
}
}
2026-07-31 16:29:10 +08:00
}