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;
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("测试表");
}
@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
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", "查看今日客资日报");
}
@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));
}
@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));
}
@Test
void sportsVenueApiRejectsRequestWhenWecomContextHasNoUserId() throws Exception {
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))
.andExpect(jsonPath("$.content").value(org.hamcrest.Matchers.containsString("UserID")));
}
@Test
void sportsVenueApiRemovesUnsupportedWecomFontTags() throws Exception {
SemanticDispatchService semantic = mock(SemanticDispatchService.class);
when(semantic.dispatchWithAttachments("tester", "daily report"))
.thenReturn(new SemanticDispatchResult(
"new: 35\n"
+ "warning: low rate",
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"));
}
private MockMvc mvc(
CozeWorkflowService coze,
SemanticDispatchService semantic,
AssistantFileStore files
) {
return MockMvcBuilders.standaloneSetup(new AssistantApiController(coze, semantic, files))
.setMessageConverters(
new MappingJackson2HttpMessageConverter(),
new ByteArrayHttpMessageConverter())
.build();
}
}