mirror of https://github.com/openclaw/openclaw.git
212 lines
5.5 KiB
TypeScript
212 lines
5.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { validateJsonSchemaValue } from "./schema-validator.js";
|
|
|
|
describe("schema validator", () => {
|
|
it("includes allowed values in enum validation errors", () => {
|
|
const res = validateJsonSchemaValue({
|
|
cacheKey: "schema-validator.test.enum",
|
|
schema: {
|
|
type: "object",
|
|
properties: {
|
|
fileFormat: {
|
|
type: "string",
|
|
enum: ["markdown", "html", "json"],
|
|
},
|
|
},
|
|
required: ["fileFormat"],
|
|
},
|
|
value: { fileFormat: "txt" },
|
|
});
|
|
|
|
expect(res.ok).toBe(false);
|
|
if (!res.ok) {
|
|
const issue = res.errors.find((entry) => entry.path === "fileFormat");
|
|
expect(issue?.message).toContain("(allowed:");
|
|
expect(issue?.allowedValues).toEqual(["markdown", "html", "json"]);
|
|
expect(issue?.allowedValuesHiddenCount).toBe(0);
|
|
}
|
|
});
|
|
|
|
it("includes allowed value in const validation errors", () => {
|
|
const res = validateJsonSchemaValue({
|
|
cacheKey: "schema-validator.test.const",
|
|
schema: {
|
|
type: "object",
|
|
properties: {
|
|
mode: {
|
|
const: "strict",
|
|
},
|
|
},
|
|
required: ["mode"],
|
|
},
|
|
value: { mode: "relaxed" },
|
|
});
|
|
|
|
expect(res.ok).toBe(false);
|
|
if (!res.ok) {
|
|
const issue = res.errors.find((entry) => entry.path === "mode");
|
|
expect(issue?.message).toContain("(allowed:");
|
|
expect(issue?.allowedValues).toEqual(["strict"]);
|
|
expect(issue?.allowedValuesHiddenCount).toBe(0);
|
|
}
|
|
});
|
|
|
|
it("truncates long allowed-value hints", () => {
|
|
const values = [
|
|
"v1",
|
|
"v2",
|
|
"v3",
|
|
"v4",
|
|
"v5",
|
|
"v6",
|
|
"v7",
|
|
"v8",
|
|
"v9",
|
|
"v10",
|
|
"v11",
|
|
"v12",
|
|
"v13",
|
|
];
|
|
const res = validateJsonSchemaValue({
|
|
cacheKey: "schema-validator.test.enum.truncate",
|
|
schema: {
|
|
type: "object",
|
|
properties: {
|
|
mode: {
|
|
type: "string",
|
|
enum: values,
|
|
},
|
|
},
|
|
required: ["mode"],
|
|
},
|
|
value: { mode: "not-listed" },
|
|
});
|
|
|
|
expect(res.ok).toBe(false);
|
|
if (!res.ok) {
|
|
const issue = res.errors.find((entry) => entry.path === "mode");
|
|
expect(issue?.message).toContain("(allowed:");
|
|
expect(issue?.message).toContain("... (+1 more)");
|
|
expect(issue?.allowedValues).toEqual([
|
|
"v1",
|
|
"v2",
|
|
"v3",
|
|
"v4",
|
|
"v5",
|
|
"v6",
|
|
"v7",
|
|
"v8",
|
|
"v9",
|
|
"v10",
|
|
"v11",
|
|
"v12",
|
|
]);
|
|
expect(issue?.allowedValuesHiddenCount).toBe(1);
|
|
}
|
|
});
|
|
|
|
it("appends missing required property to the structured path", () => {
|
|
const res = validateJsonSchemaValue({
|
|
cacheKey: "schema-validator.test.required.path",
|
|
schema: {
|
|
type: "object",
|
|
properties: {
|
|
settings: {
|
|
type: "object",
|
|
properties: {
|
|
mode: { type: "string" },
|
|
},
|
|
required: ["mode"],
|
|
},
|
|
},
|
|
required: ["settings"],
|
|
},
|
|
value: { settings: {} },
|
|
});
|
|
|
|
expect(res.ok).toBe(false);
|
|
if (!res.ok) {
|
|
const issue = res.errors.find((entry) => entry.path === "settings.mode");
|
|
expect(issue).toBeDefined();
|
|
expect(issue?.allowedValues).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
it("appends missing dependency property to the structured path", () => {
|
|
const res = validateJsonSchemaValue({
|
|
cacheKey: "schema-validator.test.dependencies.path",
|
|
schema: {
|
|
type: "object",
|
|
properties: {
|
|
settings: {
|
|
type: "object",
|
|
dependencies: {
|
|
mode: ["format"],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
value: { settings: { mode: "strict" } },
|
|
});
|
|
|
|
expect(res.ok).toBe(false);
|
|
if (!res.ok) {
|
|
const issue = res.errors.find((entry) => entry.path === "settings.format");
|
|
expect(issue).toBeDefined();
|
|
expect(issue?.allowedValues).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
it("truncates oversized allowed value entries", () => {
|
|
const oversizedAllowed = "a".repeat(300);
|
|
const res = validateJsonSchemaValue({
|
|
cacheKey: "schema-validator.test.enum.long-value",
|
|
schema: {
|
|
type: "object",
|
|
properties: {
|
|
mode: {
|
|
type: "string",
|
|
enum: [oversizedAllowed],
|
|
},
|
|
},
|
|
required: ["mode"],
|
|
},
|
|
value: { mode: "not-listed" },
|
|
});
|
|
|
|
expect(res.ok).toBe(false);
|
|
if (!res.ok) {
|
|
const issue = res.errors.find((entry) => entry.path === "mode");
|
|
expect(issue).toBeDefined();
|
|
expect(issue?.message).toContain("(allowed:");
|
|
expect(issue?.message).toContain("... (+");
|
|
}
|
|
});
|
|
|
|
it("sanitizes terminal text while preserving structured fields", () => {
|
|
const maliciousProperty = "evil\nkey\t\x1b[31mred\x1b[0m";
|
|
const res = validateJsonSchemaValue({
|
|
cacheKey: "schema-validator.test.terminal-sanitize",
|
|
schema: {
|
|
type: "object",
|
|
properties: {},
|
|
required: [maliciousProperty],
|
|
},
|
|
value: {},
|
|
});
|
|
|
|
expect(res.ok).toBe(false);
|
|
if (!res.ok) {
|
|
const issue = res.errors[0];
|
|
expect(issue).toBeDefined();
|
|
expect(issue?.path).toContain("\n");
|
|
expect(issue?.message).toContain("\n");
|
|
expect(issue?.text).toContain("\\n");
|
|
expect(issue?.text).toContain("\\t");
|
|
expect(issue?.text).not.toContain("\n");
|
|
expect(issue?.text).not.toContain("\t");
|
|
expect(issue?.text).not.toContain("\x1b");
|
|
}
|
|
});
|
|
});
|