import { describe, expect, it } from "vitest"; import { compareSchemas, generateSample, inspectWorkspace, parseSchemaDocument, validateJsonInstance, } from "../../src/schema/model"; const entry = { name: "person.schema.json", language: "json-schema" as const, source: JSON.stringify({ $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", required: ["name", "address"], properties: { name: { type: "string", minLength: 1 }, address: { $ref: "defs/address.json#/$defs/address" }, }, additionalProperties: false, }), }; const support = { name: "defs/address.json", language: "json-schema" as const, source: JSON.stringify({ $schema: "https://json-schema.org/draft/2020-12/schema", $defs: { address: { type: "object", required: ["city"], properties: { city: { type: "string" } }, }, }, }), }; describe("schema workspace", () => { it("resolves local references, generates a sample, and validates instances", () => { const workspace = inspectWorkspace([entry, support], entry.name); expect(workspace.references).toEqual([ expect.objectContaining({ target: support.name, status: "resolved" }), ]); expect(JSON.parse(generateSample(workspace).output)).toEqual({ name: "string", address: { city: "string" }, }); expect( validateJsonInstance( workspace, JSON.stringify({ name: "Ada", address: { city: "London" } }), ).valid, ).toBe(true); const invalid = validateJsonInstance( workspace, JSON.stringify({ name: "Ada" }), ); expect(invalid.valid).toBe(false); expect(invalid.diagnostics[0]?.message).toContain("required"); }); it("blocks remote and missing references without attempting resolution", () => { const workspace = inspectWorkspace([ { name: "unsafe.json", source: JSON.stringify({ $ref: "https://example.test/schema.json" }), }, { name: "missing.json", source: JSON.stringify({ $ref: "other.json" }) }, ]); expect(workspace.references.map((item) => item.status)).toEqual([ "blocked", "unresolved", ]); expect(workspace.diagnostics).toEqual( expect.arrayContaining([ expect.objectContaining({ level: "error", message: expect.stringContaining("blocked"), }), expect.objectContaining({ level: "error", message: expect.stringContaining("not supplied"), }), ]), ); }); it("refuses executable regex keywords in the bounded validator", () => { const workspace = inspectWorkspace([ { name: "pattern.json", source: JSON.stringify({ type: "string", pattern: "^(a+)+$" }), }, ]); const result = validateJsonInstance(workspace, JSON.stringify("aaaa")); expect(result.valid).toBe(false); expect(result.diagnostics[0]?.message).toMatch(/pattern.*not executed/iu); }); it("also refuses regex and dynamic-reference keywords in supplied reference documents", () => { const regexWorkspace = inspectWorkspace([ { name: "root.json", source: JSON.stringify({ $ref: "defs.json" }) }, { name: "defs.json", source: JSON.stringify({ type: "string", pattern: "^(a+)+$" }), }, ]); expect( validateJsonInstance(regexWorkspace, JSON.stringify("aaaa")).valid, ).toBe(false); const dynamicWorkspace = inspectWorkspace([ { name: "dynamic.json", source: JSON.stringify({ $dynamicRef: "#node" }), }, ]); expect( validateJsonInstance(dynamicWorkspace, JSON.stringify({})).diagnostics[0] ?.message, ).toContain("$dynamicRef"); }); it("validates the documented 2020-12 object, array, dependency, and combiner subset", () => { const workspace = inspectWorkspace([ { name: "bounded.json", source: JSON.stringify({ $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", required: ["kind", "values"], properties: { kind: { enum: ["measurement"] }, note: { type: "string", minLength: 2, maxLength: 8 }, values: { type: "array", prefixItems: [{ type: "integer" }], items: { type: "number", minimum: 0 }, contains: { const: 2 }, minContains: 1, uniqueItems: true, }, }, dependentRequired: { note: ["kind"] }, allOf: [{ minProperties: 2 }], additionalProperties: false, }), }, ]); expect( validateJsonInstance( workspace, JSON.stringify({ kind: "measurement", note: "ok", values: [1, 2] }), ).valid, ).toBe(true); const invalid = validateJsonInstance( workspace, JSON.stringify({ kind: "measurement", values: [1, 1], extra: true }), ); expect(invalid.valid).toBe(false); expect(invalid.diagnostics.map((item) => item.message).join("\n")).toMatch( /uniqueItems|contains|false schema/u, ); }); it("supports Draft 6 local files even when root identifiers are remote-looking", () => { const workspace = inspectWorkspace([ { name: "root.json", source: JSON.stringify({ $schema: "http://json-schema.org/draft-06/schema#", $id: "https://schemas.example.test/root.json", $ref: "defs.json#/$defs/value", }), }, { name: "defs.json", source: JSON.stringify({ $schema: "http://json-schema.org/draft-06/schema#", $defs: { value: { type: "integer", minimum: 1 } }, }), }, ]); expect(validateJsonInstance(workspace, "2").valid).toBe(true); expect(validateJsonInstance(workspace, "0").valid).toBe(false); }); it("rejects active XML constructs before structural inspection", () => { expect(() => parseSchemaDocument({ name: "unsafe.xsd", source: `]>`, }), ).toThrow(/DTD and entity/u); }); }); describe("schema languages", () => { it("inventories XSD and generates a clearly heuristic XML instance", () => { const workspace = inspectWorkspace([ { name: "person.xsd", source: ``, }, ]); expect(workspace.documents.get("person.xsd")?.language).toBe("xsd"); expect( workspace.diagnostics.some((item) => item.message.includes("not provided"), ), ).toBe(true); const sample = generateSample(workspace); expect(sample.output).toContain(""); expect(sample.output).toContain("string"); expect(sample.notices[0]).toContain("Heuristic XSD"); }); it("inventories Relax NG and Schematron without executing expressions", () => { const rng = inspectWorkspace([ { name: "note.rng", source: ``, }, ]); expect(generateSample(rng).output).toContain( "string", ); const sch = inspectWorkspace([ { name: "rules.sch", source: `A name is required.`, }, ]); expect(sch.documents.get("rules.sch")?.language).toBe("schematron"); expect( sch.diagnostics.some( (item) => item.message.includes("never executed") || item.message.includes("not executed"), ), ).toBe(true); }); it("parses OpenAPI YAML, inventories operations, and derives component samples", () => { const workspace = inspectWorkspace([ { name: "openapi.yaml", source: `openapi: 3.1.0 info: title: Local API version: 1.0.0 paths: /people: get: responses: "200": { description: ok } components: schemas: Person: type: object required: [name] properties: name: { type: string } `, }, ]); const document = workspace.documents.get("openapi.yaml")!; expect(document.language).toBe("openapi"); expect(document.summary).toContainEqual({ label: "Operations", value: "1", }); expect(JSON.parse(generateSample(workspace).output)).toEqual({ name: "string", }); }); }); describe("conservative comparison", () => { it("signals newly required fields and removed enum values", () => { const changes = compareSchemas( { name: "before.json", source: JSON.stringify({ type: "object", properties: { state: { enum: ["on", "off"] } }, }), }, { name: "after.json", source: JSON.stringify({ type: "object", required: ["state"], properties: { state: { enum: ["on"] } }, }), }, ); expect(changes).toEqual( expect.arrayContaining([ expect.objectContaining({ level: "incompatible", message: expect.stringContaining("required"), }), expect.objectContaining({ level: "incompatible", path: "/properties/state/enum", message: expect.stringContaining("removed"), }), ]), ); }); it("signals removed OpenAPI operations", () => { const base = { openapi: "3.1.0", info: { title: "Example", version: "1" }, }; const changes = compareSchemas( { name: "before.json", source: JSON.stringify({ ...base, paths: { "/items": { get: { responses: { "200": { description: "ok" } } } }, }, }), }, { name: "after.json", source: JSON.stringify({ ...base, paths: {} }) }, ); expect(changes).toContainEqual( expect.objectContaining({ level: "incompatible", path: "GET /items" }), ); }); });