package com.nanri.aiimage.config; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; /** * task-202:Python-contract 夹具准备契约。 * 校验 python-contract/*.json 存在、为合法 JSON、顶层字段与真实 DTO 对齐、已脱敏(无真实凭据)、 * 可复现;字段来源与脱敏口径见同目录 README.md。 */ class PythonContractFixtureTest { private static final Path DIR = Path.of(System.getProperty("user.dir"), "src/test/resources/python-contract"); private static final ObjectMapper MAPPER = new ObjectMapper(); private JsonNode json(String name) throws IOException { return MAPPER.readTree(Files.readAllBytes(DIR.resolve(name))); } private static Set keys(JsonNode node) { List names = new ArrayList<>(); node.fieldNames().forEachRemaining(names::add); return Set.copyOf(names); } @Test void fixtureResultRequestExists() throws IOException { JsonNode n = json("result.request.json"); assertEquals("sim-20260905-0001", n.path("submissionId").asText()); } @Test void fixtureItemsExists() throws IOException { JsonNode n = json("items.response.json"); assertTrue(n.path("items").isArray() && n.path("items").size() >= 1); } @Test void fixtureParsedPayloadExists() throws IOException { JsonNode n = json("parsed_payload.response.json"); assertTrue(n.has("allItems") && n.has("groups") && n.has("items")); } @Test void fixtureHeartbeatExists() throws IOException { JsonNode req = json("heartbeat.request.json"); JsonNode resp = json("heartbeat.response.json"); assertTrue(req.has("moduleType") && req.has("phase")); assertTrue(resp.has("alive") && resp.has("status")); } @Test void fixtureSanitizedNoRealCredentials() throws IOException { String all = String.join("\n", List.of( Files.readString(DIR.resolve("parsed_payload.response.json")), Files.readString(DIR.resolve("result.request.json")))); assertFalse(all.contains("WTFrb"), "不得出现真实 DB/内部凭据"); assertFalse(all.toLowerCase().contains("bearer "), "不得出现真实 token"); JsonNode parsed = json("parsed_payload.response.json"); assertEquals("", parsed.path("apiKey").asText(), "apiKey 必须占位"); assertFalse(parsed.path("aiPrompt").asText().toLowerCase().contains("真实"), "aiPrompt 不得含真实内容"); } @Test void fixtureShapeValidJson() throws IOException { for (String name : List.of("heartbeat.request.json", "heartbeat.response.json", "items.response.json", "result.request.json", "parsed_payload.response.json")) { JsonNode n = json(name); assertTrue(n.isContainerNode(), name + " 应为 JSON 对象/数组"); } } @Test void fixtureFieldsDocumentedByReadme() throws IOException { String readme = Files.readString(DIR.resolve("README.md")); assertTrue(readme.contains("TaskHeartbeatRequest") && readme.contains("CollectDataItemsPageVo") && readme.contains("SimilarAsinSubmitResultRequest") && readme.contains("SimilarAsinParsedPayloadDto"), "README 应说明字段来源"); assertTrue(readme.contains("脱敏"), "README 应说明脱敏"); } @Test void fixtureHeartbeatTopKeysMatchDto() throws IOException { assertEquals(Set.of("moduleType", "phase", "current", "total", "collectStage", "currentKeyword", "searchCurrentPage", "searchTotalPages", "detailProcessedAsins", "detailTotalAsins"), keys(json("heartbeat.request.json")), "心跳请求顶层字段应与 TaskHeartbeatRequest 对齐"); assertEquals(Set.of("alive", "status", "moduleType", "message"), keys(json("heartbeat.response.json")), "心跳响应应与 TaskHeartbeatVo 对齐"); } @Test void fixtureReproducibleStable() throws IOException { for (String name : List.of("heartbeat.request.json", "items.response.json", "result.request.json", "parsed_payload.response.json")) { String a = Files.readString(DIR.resolve(name)); String b = Files.readString(DIR.resolve(name)); assertEquals(a, b, name + " 应可复现"); } } }