task-98: 移除 similar-asin/appearance-patent 模块 Coze,状态机与共享组件改名 LLM
Build Backend JAR / build (push) Has been cancelled
Build Backend JAR / build (push) Has been cancelled
- similarasin/appearancepatent 模块全部 Coze 工作流调用改走 direct-LLM(已确认唯一运行路径) - 共享组件改名:CozeTaskQueueGate→TaskQueueGate、CozeGroupResultPropagator→GroupResultPropagator - 状态机改名:biz_task_scope_state 的 coze_* 列→llm_*、stateJson coze 键→llm(V100 迁移已应用生产) - 删除 biz_coze_credential 表、CozeCredential* 类、SimilarAsinCozeClient、AppearancePatentCozeClient→LlmClient - 前端 brand 页 Coze 文案→LLM;Python 后端删除 cozepy 依赖与死配置 - 修复 TaskResultFileJobWorker 启动失败:TaskFileJobConfig 注册 ResultFileJobHandlerRegistry 与 13 个 handler bean(含 validateCoverage 启动校验)
This commit is contained in:
+237
@@ -0,0 +1,237 @@
|
||||
package com.nanri.aiimage.common.service;
|
||||
|
||||
import com.nanri.aiimage.common.exception.BusinessException;
|
||||
import com.nanri.aiimage.common.exception.TaskOwnerMismatchException;
|
||||
import com.nanri.aiimage.config.InstanceRoutingProperties;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.mock.web.DelegatingServletInputStream;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.web.util.ContentCachingRequestWrapper;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* TaskOwnerForwardService 转发安全测试(任务 54)。
|
||||
* 反射调用私有静态方法 requestBody / copyForwardHeaders / hasAlreadyForwarded,
|
||||
* 验证 multipart 大 body 转发路径与 hop-by-hop 头剔除。
|
||||
*/
|
||||
class TaskOwnerForwardServiceTest {
|
||||
|
||||
private static final byte[] MULTIPART_BODY = (
|
||||
"-----b\r\nContent-Disposition: form-data; name=\"f\"; filename=\"a.bin\"\r\n\r\n"
|
||||
+ "x".repeat(2048) + "\r\n-----b--\r\n").getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
private static Object invoke(String name, Class<?>[] paramTypes, Object... args) throws Exception {
|
||||
Method method = TaskOwnerForwardService.class.getDeclaredMethod(name, paramTypes);
|
||||
method.setAccessible(true);
|
||||
try {
|
||||
return method.invoke(null, args);
|
||||
} catch (InvocationTargetException e) {
|
||||
throw (Exception) e.getCause();
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] requestBody(HttpServletRequest request) throws Exception {
|
||||
return (byte[]) invoke("requestBody", new Class<?>[]{HttpServletRequest.class}, request);
|
||||
}
|
||||
|
||||
private static HttpHeaders forwardHeaders(HttpServletRequest request, String currentInstanceId) throws Exception {
|
||||
return (HttpHeaders) invoke("copyForwardHeaders",
|
||||
new Class<?>[]{HttpServletRequest.class, String.class}, request, currentInstanceId);
|
||||
}
|
||||
|
||||
private static boolean alreadyForwarded(HttpServletRequest request) throws Exception {
|
||||
return (boolean) invoke("hasAlreadyForwarded", new Class<?>[]{HttpServletRequest.class}, request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void nonWrapperReadsInputStream() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/forward");
|
||||
request.setContent(MULTIPART_BODY);
|
||||
assertArrayEquals(MULTIPART_BODY, requestBody(request), "非 wrapper 走 inputStream 路径读完整 body");
|
||||
}
|
||||
|
||||
@Test
|
||||
void wrapperReadsCache() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/forward");
|
||||
request.setContent(MULTIPART_BODY);
|
||||
ContentCachingRequestWrapper wrapper = new ContentCachingRequestWrapper(request);
|
||||
byte[] body = requestBody(wrapper);
|
||||
assertArrayEquals(new byte[0], body, "wrapper 路径读缓存:body 未被消费时缓存为空数组");
|
||||
byte[] content = wrapper.getContentAsByteArray();
|
||||
assertTrue(content.length == 0, "未消费则缓存仍为空");
|
||||
wrapper.getInputStream().readAllBytes();
|
||||
assertArrayEquals(MULTIPART_BODY, requestBody(wrapper), "消费后缓存与原始 body 一致");
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipartBodyForwardOk() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/upload");
|
||||
request.setContentType("multipart/form-data; boundary=----b");
|
||||
request.setContent(MULTIPART_BODY);
|
||||
assertArrayEquals(MULTIPART_BODY, requestBody(request), "multipart 转发 body 与原请求字节一致");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ioErrorHasMessage() {
|
||||
// 伪造读流即抛 IOException 的 request:read() 抛 IOException,StreamUtils 原样上抛
|
||||
java.io.InputStream failing = new java.io.InputStream() {
|
||||
@Override
|
||||
public int read() throws java.io.IOException {
|
||||
throw new java.io.IOException("stream closed");
|
||||
}
|
||||
};
|
||||
MockHttpServletRequest broken = new MockHttpServletRequest("POST", "/api/x") {
|
||||
@Override
|
||||
public jakarta.servlet.ServletInputStream getInputStream() {
|
||||
return new DelegatingServletInputStream(failing);
|
||||
}
|
||||
};
|
||||
broken.setContent("x".getBytes(StandardCharsets.UTF_8));
|
||||
BusinessException ex = assertThrows(BusinessException.class,
|
||||
() -> invoke("requestBody", new Class<?>[]{HttpServletRequest.class}, broken),
|
||||
"IO 异常包装为 BusinessException");
|
||||
assertTrue(ex.getMessage().contains("读取转发请求体失败"), "异常消息携带上下文:" + ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void hopByHopHeadersRemoved() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/forward");
|
||||
request.addHeader("Connection", "keep-alive");
|
||||
request.addHeader("Transfer-Encoding", "chunked");
|
||||
request.addHeader("Host", "example.com");
|
||||
request.addHeader("Content-Length", "123");
|
||||
request.addHeader("X-Custom", "keep-me");
|
||||
HttpHeaders headers = forwardHeaders(request, "server-110");
|
||||
assertFalse(headers.containsKey("Connection"), "connection 剔除");
|
||||
assertFalse(headers.containsKey("Transfer-Encoding"), "transfer-encoding 剔除");
|
||||
assertFalse(headers.containsKey("Host"), "host 剔除");
|
||||
assertFalse(headers.containsKey("Content-Length"), "content-length 剔除");
|
||||
assertEquals("keep-me", headers.getFirst("X-Custom"), "普通头保留");
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeaderSet() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/forward");
|
||||
HttpHeaders headers = forwardHeaders(request, "server-121");
|
||||
assertEquals("server-121", headers.getFirst(TaskOwnerForwardService.FORWARDED_HEADER),
|
||||
"转发头标记当前实例");
|
||||
}
|
||||
|
||||
@Test
|
||||
void contentTypeKept() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/forward");
|
||||
request.setContentType("multipart/form-data; boundary=----b");
|
||||
HttpHeaders headers = forwardHeaders(request, "server-110");
|
||||
assertEquals("multipart/form-data; boundary=----b", headers.getFirst(HttpHeaders.CONTENT_TYPE),
|
||||
"content-type 保留");
|
||||
}
|
||||
|
||||
@Test
|
||||
void loopDetected() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/forward");
|
||||
request.addHeader(TaskOwnerForwardService.FORWARDED_HEADER, "server-110");
|
||||
assertTrue(alreadyForwarded(request), "已带转发头判定为循环");
|
||||
MockHttpServletRequest fresh = new MockHttpServletRequest("POST", "/api/forward");
|
||||
assertFalse(alreadyForwarded(fresh), "无转发头不判定循环");
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardExceptionCarriesLoopMessage() {
|
||||
TaskOwnerMismatchException ex = new TaskOwnerMismatchException(
|
||||
1L, "op", "server-110", "server-121");
|
||||
InstanceRoutingProperties properties = new InstanceRoutingProperties();
|
||||
properties.setRoutes(Map.of("server-110", "http://10.0.0.1:18080"));
|
||||
TaskOwnerForwardService service = new TaskOwnerForwardService(properties);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/forward");
|
||||
request.addHeader(TaskOwnerForwardService.FORWARDED_HEADER, "server-110");
|
||||
BusinessException loopEx = assertThrows(BusinessException.class,
|
||||
() -> service.forwardCurrentRequest(ex, request),
|
||||
"循环转发拒绝并抛 BusinessException");
|
||||
assertTrue(loopEx.getMessage().contains("循环"), "循环消息:" + loopEx.getMessage());
|
||||
}
|
||||
|
||||
private static byte[] forwardBodyThroughWrapper(byte[] content, String contentType) throws Exception {
|
||||
return forwardBodyThroughWrapper(content, contentType, 1024 * 1024);
|
||||
}
|
||||
|
||||
private static byte[] forwardBodyThroughWrapper(byte[] content, String contentType, int cacheLimitBytes) throws Exception {
|
||||
// 模拟真实链路:过滤器包装 → 业务读取(填充缓存)→ 转发读缓存
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/upload");
|
||||
request.setContentType(contentType);
|
||||
request.setContent(content);
|
||||
ContentCachingRequestWrapper wrapper = new ContentCachingRequestWrapper(request, cacheLimitBytes);
|
||||
wrapper.getInputStream().readAllBytes();
|
||||
return requestBody(wrapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardJsonBytesEqual() throws Exception {
|
||||
byte[] body = "{\"taskId\":100,\"status\":\"RUNNING\"}".getBytes(StandardCharsets.UTF_8);
|
||||
assertArrayEquals(body, forwardBodyThroughWrapper(body, "application/json"), "json 字节级一致");
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardMultipartBytesEqual() throws Exception {
|
||||
assertArrayEquals(MULTIPART_BODY, forwardBodyThroughWrapper(MULTIPART_BODY, "multipart/form-data; boundary=----b"),
|
||||
"multipart 字节级一致");
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardEmptyBodySafe() throws Exception {
|
||||
byte[] empty = new byte[0];
|
||||
assertArrayEquals(empty, forwardBodyThroughWrapper(empty, "application/json"), "空 body 安全");
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/empty");
|
||||
assertArrayEquals(empty, requestBody(request), "非包装空 body 返回空数组");
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardLargeBodyEqual() throws Exception {
|
||||
byte[] large = ("{\"payload\":\"" + "x".repeat(1024 * 1024 + 100) + "\"}").getBytes(StandardCharsets.UTF_8);
|
||||
assertArrayEquals(large, forwardBodyThroughWrapper(large, "application/json", 2 * 1024 * 1024), ">1MB 大 body 一致");
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardUtf8ChineseEqual() throws Exception {
|
||||
byte[] body = "{\"name\":\"任务进度查询\",\"备注\":\"成功\"}".getBytes(StandardCharsets.UTF_8);
|
||||
assertArrayEquals(body, forwardBodyThroughWrapper(body, "application/json; charset=utf-8"), "中文 UTF-8 一致");
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardSpecialCharsEqual() throws Exception {
|
||||
byte[] body = "{\"s\":\"a\\n\\t\\\"b\\\\céü中\"}".getBytes(StandardCharsets.UTF_8);
|
||||
assertArrayEquals(body, forwardBodyThroughWrapper(body, "application/json"), "转义与特殊字符一致");
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardMultipleChunksEqual() throws Exception {
|
||||
StringBuilder sb = new StringBuilder("{\"chunks\":[");
|
||||
for (int i = 0; i < 50; i++) {
|
||||
if (i > 0) sb.append(',');
|
||||
sb.append('"').append("chunk-").append(i).append('"');
|
||||
}
|
||||
sb.append("]}");
|
||||
byte[] body = sb.toString().getBytes(StandardCharsets.UTF_8);
|
||||
assertArrayEquals(body, forwardBodyThroughWrapper(body, "application/json"), "多段内容一致");
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardIdentityRepeated() throws Exception {
|
||||
byte[] body = "{\"repeat\":\"yes\"}".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] first = forwardBodyThroughWrapper(body, "application/json");
|
||||
byte[] second = forwardBodyThroughWrapper(body, "application/json");
|
||||
assertArrayEquals(first, second, "两次转发结果一致");
|
||||
assertArrayEquals(body, second, "与原始一致");
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -6,7 +6,7 @@ import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class CozeGroupResultPropagatorTest {
|
||||
class GroupResultPropagatorTest {
|
||||
|
||||
@Test
|
||||
void doesNotTreatNoInfringementAsInfringementHit() {
|
||||
@@ -19,7 +19,7 @@ class CozeGroupResultPropagatorTest {
|
||||
new ResultRow("无侵权")
|
||||
);
|
||||
|
||||
int updatedRows = CozeGroupResultPropagator.propagateByGroup(
|
||||
int updatedRows = GroupResultPropagator.propagateByGroup(
|
||||
parsedRows,
|
||||
ParsedRow::displayId,
|
||||
row -> resultRows.get(parsedRows.indexOf(row)),
|
||||
@@ -44,7 +44,7 @@ class CozeGroupResultPropagatorTest {
|
||||
new ResultRow("侵权")
|
||||
);
|
||||
|
||||
int updatedRows = CozeGroupResultPropagator.propagateByGroup(
|
||||
int updatedRows = GroupResultPropagator.propagateByGroup(
|
||||
parsedRows,
|
||||
ParsedRow::displayId,
|
||||
row -> resultRows.get(parsedRows.indexOf(row)),
|
||||
Reference in New Issue
Block a user