|
@@ -0,0 +1,223 @@
|
|
|
|
|
+package com.agent.management.engine;
|
|
|
|
|
+
|
|
|
|
|
+import com.fasterxml.jackson.databind.JsonNode;
|
|
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
|
|
+
|
|
|
|
|
+import java.util.LinkedHashMap;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
+
|
|
|
|
|
+import static org.assertj.core.api.Assertions.assertThat;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * {@link OutputValidator} 单元测试。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>覆盖:必填校验、类型校验、类型自动转换、综合场景。</p>
|
|
|
|
|
+ */
|
|
|
|
|
+class OutputValidatorTest {
|
|
|
|
|
+
|
|
|
|
|
+ private static final ObjectMapper MAPPER = new ObjectMapper();
|
|
|
|
|
+
|
|
|
|
|
+ private static JsonNode outputsDecl(String... fields) {
|
|
|
|
|
+ // fields 形如 "name|type|required" 或 "name|type"
|
|
|
|
|
+ StringBuilder sb = new StringBuilder("[");
|
|
|
|
|
+ for (int i = 0; i < fields.length; i++) {
|
|
|
|
|
+ if (i > 0) sb.append(",");
|
|
|
|
|
+ String[] parts = fields[i].split("\\|");
|
|
|
|
|
+ sb.append("{\"name\":\"").append(parts[0]).append("\",")
|
|
|
|
|
+ .append("\"type\":\"").append(parts[1]).append("\"");
|
|
|
|
|
+ if (parts.length > 2 && "required".equalsIgnoreCase(parts[2])) {
|
|
|
|
|
+ sb.append(",\"required\":true");
|
|
|
|
|
+ }
|
|
|
|
|
+ sb.append("}");
|
|
|
|
|
+ }
|
|
|
|
|
+ sb.append("]");
|
|
|
|
|
+ try {
|
|
|
|
|
+ return MAPPER.readTree(sb.toString());
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ throw new RuntimeException(e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void allRequiredPresentAndTypesMatchPasses() {
|
|
|
|
|
+ JsonNode decl = outputsDecl("name|string|required", "age|number");
|
|
|
|
|
+ Map<String, Object> parsed = new LinkedHashMap<>();
|
|
|
|
|
+ parsed.put("name", "Alice");
|
|
|
|
|
+ parsed.put("age", 18);
|
|
|
|
|
+
|
|
|
|
|
+ OutputValidator.ValidationResult result = OutputValidator.validate(parsed, decl);
|
|
|
|
|
+
|
|
|
|
|
+ assertThat(result.ok).isTrue();
|
|
|
|
|
+ assertThat(result.missingRequired).isEmpty();
|
|
|
|
|
+ assertThat(result.typeMismatched).isEmpty();
|
|
|
|
|
+ assertThat(result.converted).containsEntry("name", "Alice").containsEntry("age", 18);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void missingRequiredFieldAddedToMissingList() {
|
|
|
|
|
+ JsonNode decl = outputsDecl("name|string|required", "age|number|required");
|
|
|
|
|
+ Map<String, Object> parsed = new LinkedHashMap<>();
|
|
|
|
|
+ parsed.put("name", "Alice");
|
|
|
|
|
+ // age 缺失
|
|
|
|
|
+
|
|
|
|
|
+ OutputValidator.ValidationResult result = OutputValidator.validate(parsed, decl);
|
|
|
|
|
+
|
|
|
|
|
+ assertThat(result.ok).isFalse();
|
|
|
|
|
+ assertThat(result.missingRequired).containsExactly("age");
|
|
|
|
|
+ assertThat(result.typeMismatched).isEmpty();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void nullRequiredFieldTreatedAsMissing() {
|
|
|
|
|
+ JsonNode decl = outputsDecl("name|string|required");
|
|
|
|
|
+ Map<String, Object> parsed = new LinkedHashMap<>();
|
|
|
|
|
+ parsed.put("name", null);
|
|
|
|
|
+
|
|
|
|
|
+ OutputValidator.ValidationResult result = OutputValidator.validate(parsed, decl);
|
|
|
|
|
+
|
|
|
|
|
+ assertThat(result.ok).isFalse();
|
|
|
|
|
+ assertThat(result.missingRequired).containsExactly("name");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void blankStringRequiredFieldTreatedAsMissing() {
|
|
|
|
|
+ JsonNode decl = outputsDecl("name|string|required");
|
|
|
|
|
+ Map<String, Object> parsed = new LinkedHashMap<>();
|
|
|
|
|
+ parsed.put("name", " ");
|
|
|
|
|
+
|
|
|
|
|
+ OutputValidator.ValidationResult result = OutputValidator.validate(parsed, decl);
|
|
|
|
|
+
|
|
|
|
|
+ assertThat(result.ok).isFalse();
|
|
|
|
|
+ assertThat(result.missingRequired).containsExactly("name");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void typeMismatchNumberWithArrayAddedToMismatched() {
|
|
|
|
|
+ JsonNode decl = outputsDecl("age|number");
|
|
|
|
|
+ Map<String, Object> parsed = new LinkedHashMap<>();
|
|
|
|
|
+ parsed.put("age", List.of(1, 2, 3)); // array 不能转 number
|
|
|
|
|
+
|
|
|
|
|
+ OutputValidator.ValidationResult result = OutputValidator.validate(parsed, decl);
|
|
|
|
|
+
|
|
|
|
|
+ assertThat(result.ok).isFalse();
|
|
|
|
|
+ assertThat(result.typeMismatched).anyMatch(s -> s.startsWith("age("));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void typeMismatchBooleanWithObjectAddedToMismatched() {
|
|
|
|
|
+ JsonNode decl = outputsDecl("active|boolean");
|
|
|
|
|
+ Map<String, Object> parsed = new LinkedHashMap<>();
|
|
|
|
|
+ parsed.put("active", Map.of("x", 1)); // object 不能转 boolean
|
|
|
|
|
+
|
|
|
|
|
+ OutputValidator.ValidationResult result = OutputValidator.validate(parsed, decl);
|
|
|
|
|
+
|
|
|
|
|
+ assertThat(result.ok).isFalse();
|
|
|
|
|
+ assertThat(result.typeMismatched).anyMatch(s -> s.startsWith("active("));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void autoConvertStringNumberToNumber() {
|
|
|
|
|
+ JsonNode decl = outputsDecl("age|number");
|
|
|
|
|
+ Map<String, Object> parsed = new LinkedHashMap<>();
|
|
|
|
|
+ parsed.put("age", "42");
|
|
|
|
|
+
|
|
|
|
|
+ OutputValidator.ValidationResult result = OutputValidator.validate(parsed, decl);
|
|
|
|
|
+
|
|
|
|
|
+ assertThat(result.ok).isTrue();
|
|
|
|
|
+ assertThat(result.converted.get("age")).isInstanceOf(Number.class);
|
|
|
|
|
+ assertThat(((Number) result.converted.get("age")).intValue()).isEqualTo(42);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void autoConvertStringTrueToBoolean() {
|
|
|
|
|
+ JsonNode decl = outputsDecl("active|boolean");
|
|
|
|
|
+ Map<String, Object> parsed = new LinkedHashMap<>();
|
|
|
|
|
+ parsed.put("active", "true");
|
|
|
|
|
+
|
|
|
|
|
+ OutputValidator.ValidationResult result = OutputValidator.validate(parsed, decl);
|
|
|
|
|
+
|
|
|
|
|
+ assertThat(result.ok).isTrue();
|
|
|
|
|
+ assertThat(result.converted.get("active")).isEqualTo(Boolean.TRUE);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void autoConvertSingleValueToArray() {
|
|
|
|
|
+ JsonNode decl = outputsDecl("tags|array");
|
|
|
|
|
+ Map<String, Object> parsed = new LinkedHashMap<>();
|
|
|
|
|
+ parsed.put("tags", "hello");
|
|
|
|
|
+
|
|
|
|
|
+ OutputValidator.ValidationResult result = OutputValidator.validate(parsed, decl);
|
|
|
|
|
+
|
|
|
|
|
+ assertThat(result.ok).isTrue();
|
|
|
|
|
+ assertThat(result.converted.get("tags")).isInstanceOf(List.class);
|
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
|
|
+ List<Object> tagsList = (List<Object>) result.converted.get("tags");
|
|
|
|
|
+ assertThat(tagsList).containsExactly("hello");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void anyValueConvertsToString() {
|
|
|
|
|
+ JsonNode decl = outputsDecl("label|string");
|
|
|
|
|
+ Map<String, Object> parsed = new LinkedHashMap<>();
|
|
|
|
|
+ parsed.put("label", 42);
|
|
|
|
|
+
|
|
|
|
|
+ OutputValidator.ValidationResult result = OutputValidator.validate(parsed, decl);
|
|
|
|
|
+
|
|
|
|
|
+ assertThat(result.ok).isTrue();
|
|
|
|
|
+ assertThat(result.converted.get("label")).isEqualTo("42");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void nonRequiredNullSkipped() {
|
|
|
|
|
+ JsonNode decl = outputsDecl("name|string|required", "nickname|string");
|
|
|
|
|
+ Map<String, Object> parsed = new LinkedHashMap<>();
|
|
|
|
|
+ parsed.put("name", "Alice");
|
|
|
|
|
+ // nickname 为 null 但非必填 → 跳过
|
|
|
|
|
+
|
|
|
|
|
+ OutputValidator.ValidationResult result = OutputValidator.validate(parsed, decl);
|
|
|
|
|
+
|
|
|
|
|
+ assertThat(result.ok).isTrue();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void emptyOutputsDeclAlwaysOk() {
|
|
|
|
|
+ JsonNode decl = outputsDecl();
|
|
|
|
|
+ Map<String, Object> parsed = Map.of("anything", "value");
|
|
|
|
|
+
|
|
|
|
|
+ OutputValidator.ValidationResult result = OutputValidator.validate(parsed, decl);
|
|
|
|
|
+
|
|
|
|
|
+ assertThat(result.ok).isTrue();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void nullParsedReturnsFail() {
|
|
|
|
|
+ JsonNode decl = outputsDecl("name|string|required");
|
|
|
|
|
+
|
|
|
|
|
+ OutputValidator.ValidationResult result = OutputValidator.validate(null, decl);
|
|
|
|
|
+
|
|
|
|
|
+ assertThat(result.ok).isFalse();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void combinedMissingAndMismatchReported() {
|
|
|
|
|
+ JsonNode decl = outputsDecl(
|
|
|
|
|
+ "name|string|required",
|
|
|
|
|
+ "age|number|required",
|
|
|
|
|
+ "tags|array",
|
|
|
|
|
+ "active|boolean"
|
|
|
|
|
+ );
|
|
|
|
|
+ Map<String, Object> parsed = new LinkedHashMap<>();
|
|
|
|
|
+ // name 缺失 → missingRequired
|
|
|
|
|
+ parsed.put("age", List.of(1, 2)); // 类型不匹配 → typeMismatched
|
|
|
|
|
+ parsed.put("tags", "single"); // 自动转换 OK
|
|
|
|
|
+ parsed.put("active", Map.of("x", 1)); // 类型不匹配 → typeMismatched
|
|
|
|
|
+
|
|
|
|
|
+ OutputValidator.ValidationResult result = OutputValidator.validate(parsed, decl);
|
|
|
|
|
+
|
|
|
|
|
+ assertThat(result.ok).isFalse();
|
|
|
|
|
+ assertThat(result.missingRequired).containsExactly("name");
|
|
|
|
|
+ assertThat(result.typeMismatched).hasSize(2);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|