684 lines
21 KiB
Go
684 lines
21 KiB
Go
package validate
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"testing/fstest"
|
|
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
|
)
|
|
|
|
func TestStandardValidatorNoneSkipped(t *testing.T) {
|
|
v := NewStandardValidator("")
|
|
|
|
res, err := v.Validate(context.Background(), &domain.Artifact{Body: []byte("ignored")}, domain.OutputContract{
|
|
ValidationMode: domain.ValidationNone,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if res.Status != domain.ValidationSkipped {
|
|
t.Fatalf("expected skipped, got %q", res.Status)
|
|
}
|
|
if !res.IsValid {
|
|
t.Fatal("expected valid=true for skipped")
|
|
}
|
|
}
|
|
|
|
func TestStandardValidatorBasicSuccess(t *testing.T) {
|
|
v := NewStandardValidator("")
|
|
|
|
res, err := v.Validate(context.Background(), &domain.Artifact{Body: []byte("hello")}, domain.OutputContract{
|
|
ValidationMode: domain.ValidationBasic,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if res.Status != domain.ValidationPassed || !res.IsValid {
|
|
t.Fatalf("expected passed/valid, got status=%q valid=%v", res.Status, res.IsValid)
|
|
}
|
|
}
|
|
|
|
func TestStandardValidatorBasicFailureEmpty(t *testing.T) {
|
|
v := NewStandardValidator("")
|
|
|
|
res, err := v.Validate(context.Background(), &domain.Artifact{Body: []byte(" \n\t ")}, domain.OutputContract{
|
|
ValidationMode: domain.ValidationBasic,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if res.Status != domain.ValidationFailed || res.IsValid {
|
|
t.Fatalf("expected failed/invalid, got status=%q valid=%v", res.Status, res.IsValid)
|
|
}
|
|
if len(res.Errors) == 0 {
|
|
t.Fatal("expected validation errors")
|
|
}
|
|
}
|
|
|
|
func TestStandardValidatorJSONSuccess(t *testing.T) {
|
|
v := NewStandardValidator("")
|
|
|
|
res, err := v.Validate(context.Background(), &domain.Artifact{Body: []byte(`{"ok":true}`)}, domain.OutputContract{
|
|
ValidationMode: domain.ValidationJSON,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if res.Status != domain.ValidationPassed || !res.IsValid {
|
|
t.Fatalf("expected passed/valid, got status=%q valid=%v", res.Status, res.IsValid)
|
|
}
|
|
}
|
|
|
|
func TestStandardValidatorJSONFailure(t *testing.T) {
|
|
v := NewStandardValidator("")
|
|
|
|
res, err := v.Validate(context.Background(), &domain.Artifact{Body: []byte(`{"ok":`)}, domain.OutputContract{
|
|
ValidationMode: domain.ValidationJSON,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if res.Status != domain.ValidationFailed || res.IsValid {
|
|
t.Fatalf("expected failed/invalid, got status=%q valid=%v", res.Status, res.IsValid)
|
|
}
|
|
if len(res.Errors) == 0 {
|
|
t.Fatal("expected parse errors")
|
|
}
|
|
}
|
|
|
|
func TestStandardValidatorJSONSchemaSuccess(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
schemaPath := filepath.Join(tmp, "schema.json")
|
|
if err := os.WriteFile(schemaPath, []byte(`{
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"type": "object",
|
|
"required": ["name"],
|
|
"properties": {
|
|
"name": {"type": "string"}
|
|
}
|
|
}`), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
v := NewStandardValidator(tmp)
|
|
|
|
res, err := v.Validate(context.Background(), &domain.Artifact{Body: []byte(`{"name":"eris"}`)}, domain.OutputContract{
|
|
ValidationMode: domain.ValidationJSONSchema,
|
|
SchemaPath: "schema.json",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if res.Status != domain.ValidationPassed || !res.IsValid {
|
|
t.Fatalf("expected passed/valid, got status=%q valid=%v", res.Status, res.IsValid)
|
|
}
|
|
}
|
|
|
|
func TestStandardValidatorPreparedSchemaSurvivesSourceRemoval(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
rootSchema := []byte(`{
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"title": "original root",
|
|
"type": "object",
|
|
"required": ["value"],
|
|
"properties": {
|
|
"value": {"$ref": "value.json"}
|
|
}
|
|
}`)
|
|
rootPath := filepath.Join(tmp, "schema.json")
|
|
referencePath := filepath.Join(tmp, "value.json")
|
|
if err := os.WriteFile(rootPath, rootSchema, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(referencePath, []byte(`{
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"type": "integer",
|
|
"minimum": 2
|
|
}`), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
validator := NewStandardValidator(tmp)
|
|
preparer, ok := validator.(ValidationPreparer)
|
|
if !ok {
|
|
t.Fatal("standard validator does not support validation preparation")
|
|
}
|
|
prepared, err := preparer.PrepareValidation(context.Background(), domain.OutputContract{
|
|
ValidationMode: domain.ValidationJSONSchema,
|
|
SchemaPath: "schema.json",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("prepare validation: %v", err)
|
|
}
|
|
assertSchemaDocument(t, prepared.SchemaDocument(), rootSchema)
|
|
|
|
if err := os.Remove(rootPath); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Remove(referencePath); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
valid, err := prepared.Validate(context.Background(), &domain.Artifact{Body: []byte(`{"value":3}`)})
|
|
if err != nil {
|
|
t.Fatalf("validate prepared artifact: %v", err)
|
|
}
|
|
if valid.Status != domain.ValidationPassed || !valid.IsValid {
|
|
t.Fatalf("expected passed/valid, got status=%q valid=%v errors=%v", valid.Status, valid.IsValid, valid.Errors)
|
|
}
|
|
|
|
invalid, err := prepared.Validate(context.Background(), &domain.Artifact{Body: []byte(`{"value":"changed"}`)})
|
|
if err != nil {
|
|
t.Fatalf("validate prepared artifact: %v", err)
|
|
}
|
|
if invalid.Status != domain.ValidationFailed || invalid.IsValid {
|
|
t.Fatalf("expected failed/invalid, got status=%q valid=%v", invalid.Status, invalid.IsValid)
|
|
}
|
|
}
|
|
|
|
func TestStandardValidatorJSONSchemaNestedSchemaPathSuccess(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
nestedDir := filepath.Join(tmp, "dnd")
|
|
if err := os.MkdirAll(nestedDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(nestedDir, "schema.json"), []byte(`{
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"type": "object",
|
|
"required": ["name"],
|
|
"properties": {
|
|
"name": {"type": "string"}
|
|
}
|
|
}`), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
v := NewStandardValidator(tmp)
|
|
|
|
res, err := v.Validate(context.Background(), &domain.Artifact{Body: []byte(`{"name":"eris"}`)}, domain.OutputContract{
|
|
ValidationMode: domain.ValidationJSONSchema,
|
|
SchemaPath: filepath.Join("dnd", "schema.json"),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if res.Status != domain.ValidationPassed || !res.IsValid {
|
|
t.Fatalf("expected passed/valid, got status=%q valid=%v", res.Status, res.IsValid)
|
|
}
|
|
}
|
|
|
|
func TestStandardValidatorJSONSchemaNestedSchemaPathMissing(t *testing.T) {
|
|
v := NewStandardValidator(t.TempDir())
|
|
|
|
_, err := v.Validate(context.Background(), &domain.Artifact{Body: []byte(`{"name":"eris"}`)}, domain.OutputContract{
|
|
ValidationMode: domain.ValidationJSONSchema,
|
|
SchemaPath: filepath.Join("dnd", "missing.json"),
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected nested schema load error")
|
|
}
|
|
}
|
|
|
|
func TestStandardValidatorJSONSchemaFailure(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
schemaPath := filepath.Join(tmp, "schema.json")
|
|
if err := os.WriteFile(schemaPath, []byte(`{
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"type": "object",
|
|
"required": ["name"],
|
|
"properties": {
|
|
"name": {"type": "string"}
|
|
}
|
|
}`), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
v := NewStandardValidator(tmp)
|
|
|
|
res, err := v.Validate(context.Background(), &domain.Artifact{Body: []byte(`{"count":1}`)}, domain.OutputContract{
|
|
ValidationMode: domain.ValidationJSONSchema,
|
|
SchemaPath: "schema.json",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if res.Status != domain.ValidationFailed || res.IsValid {
|
|
t.Fatalf("expected failed/invalid, got status=%q valid=%v", res.Status, res.IsValid)
|
|
}
|
|
if len(res.Errors) == 0 {
|
|
t.Fatal("expected schema errors")
|
|
}
|
|
}
|
|
|
|
func TestStandardValidatorJSONSchemaSchemaLoadError(t *testing.T) {
|
|
v := NewStandardValidator(t.TempDir())
|
|
|
|
_, err := v.Validate(context.Background(), &domain.Artifact{Body: []byte(`{"name":"eris"}`)}, domain.OutputContract{
|
|
ValidationMode: domain.ValidationJSONSchema,
|
|
SchemaPath: "missing.json",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected schema load error")
|
|
}
|
|
}
|
|
|
|
func TestStandardValidatorJSONSchemaCompilationError(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(tmp, "schema.json"), []byte(`{"type":42}`), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
v := NewStandardValidator(tmp)
|
|
|
|
res, err := v.Validate(context.Background(), &domain.Artifact{Body: []byte(`{}`)}, domain.OutputContract{
|
|
ValidationMode: domain.ValidationJSONSchema,
|
|
SchemaPath: "schema.json",
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "failed to compile JSON schema") {
|
|
t.Fatalf("expected schema compilation error, got result=%#v error=%v", res, err)
|
|
}
|
|
}
|
|
|
|
func TestStandardValidatorLoadSchemaDocumentSuccess(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(tmp, "schema.json"), []byte(`{
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {"type": "string"}
|
|
}
|
|
}`), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
v := NewStandardValidator(tmp)
|
|
loader, ok := v.(SchemaDocumentLoader)
|
|
if !ok {
|
|
t.Fatal("standard validator must implement SchemaDocumentLoader")
|
|
}
|
|
|
|
doc, err := loader.LoadSchemaDocument(context.Background(), "schema.json")
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
|
|
obj, ok := doc.(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected object document, got %#v", doc)
|
|
}
|
|
if obj["type"] != "object" {
|
|
t.Fatalf("expected schema type=object, got %#v", obj["type"])
|
|
}
|
|
}
|
|
|
|
func TestStandardValidatorLoadSchemaDocumentInvalidJSON(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(tmp, "schema.json"), []byte(`{`), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
v := NewStandardValidator(tmp)
|
|
loader, ok := v.(SchemaDocumentLoader)
|
|
if !ok {
|
|
t.Fatal("standard validator must implement SchemaDocumentLoader")
|
|
}
|
|
|
|
_, err := loader.LoadSchemaDocument(context.Background(), "schema.json")
|
|
if err == nil {
|
|
t.Fatal("expected decode error")
|
|
}
|
|
}
|
|
|
|
func TestFSValidatorJSONSchemaSuccess(t *testing.T) {
|
|
v := NewFSValidator(fstest.MapFS{
|
|
"schemas/events.schema.json": &fstest.MapFile{Data: []byte(`{
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"type": "object",
|
|
"required": ["events"],
|
|
"properties": {
|
|
"events": {"type": "array"}
|
|
}
|
|
}`)},
|
|
}, "schemas")
|
|
|
|
res, err := v.Validate(context.Background(), &domain.Artifact{Body: []byte(`{"events":[]}`)}, domain.OutputContract{
|
|
ValidationMode: domain.ValidationJSONSchema,
|
|
SchemaPath: "events.schema.json",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if res.Status != domain.ValidationPassed || !res.IsValid {
|
|
t.Fatalf("expected passed/valid, got status=%q valid=%v", res.Status, res.IsValid)
|
|
}
|
|
}
|
|
|
|
func TestFSValidatorPreparedSchemaSurvivesSourceMutation(t *testing.T) {
|
|
rootSchema := []byte(`{
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"title": "original root",
|
|
"type": "object",
|
|
"required": ["value"],
|
|
"properties": {
|
|
"value": {"$ref": "value.json"}
|
|
}
|
|
}`)
|
|
fsys := fstest.MapFS{
|
|
"schema.json": &fstest.MapFile{Data: rootSchema},
|
|
"value.json": &fstest.MapFile{Data: []byte(`{
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"type": "integer",
|
|
"minimum": 2
|
|
}`)},
|
|
}
|
|
validator := NewFSValidator(fsys, ".")
|
|
preparer, ok := validator.(ValidationPreparer)
|
|
if !ok {
|
|
t.Fatal("filesystem validator does not support validation preparation")
|
|
}
|
|
prepared, err := preparer.PrepareValidation(context.Background(), domain.OutputContract{
|
|
ValidationMode: domain.ValidationJSONSchema,
|
|
SchemaPath: "schema.json",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("prepare validation: %v", err)
|
|
}
|
|
assertSchemaDocument(t, prepared.SchemaDocument(), rootSchema)
|
|
|
|
fsys["schema.json"] = &fstest.MapFile{Data: []byte(`{
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"type": "string"
|
|
}`)}
|
|
fsys["value.json"] = &fstest.MapFile{Data: []byte(`{
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"type": "string"
|
|
}`)}
|
|
|
|
valid, err := prepared.Validate(context.Background(), &domain.Artifact{Body: []byte(`{"value":3}`)})
|
|
if err != nil {
|
|
t.Fatalf("validate prepared artifact: %v", err)
|
|
}
|
|
if valid.Status != domain.ValidationPassed || !valid.IsValid {
|
|
t.Fatalf("expected passed/valid, got status=%q valid=%v errors=%v", valid.Status, valid.IsValid, valid.Errors)
|
|
}
|
|
|
|
invalid, err := prepared.Validate(context.Background(), &domain.Artifact{Body: []byte(`{"value":"changed"}`)})
|
|
if err != nil {
|
|
t.Fatalf("validate prepared artifact: %v", err)
|
|
}
|
|
if invalid.Status != domain.ValidationFailed || invalid.IsValid {
|
|
t.Fatalf("expected failed/invalid, got status=%q valid=%v", invalid.Status, invalid.IsValid)
|
|
}
|
|
}
|
|
|
|
func TestFSValidatorJSONSchemaRegistrationError(t *testing.T) {
|
|
v := NewFSValidator(fstest.MapFS{
|
|
"schemas/%zz.json": &fstest.MapFile{Data: []byte(`{"type":"object"}`)},
|
|
}, "schemas")
|
|
|
|
res, err := v.Validate(context.Background(), &domain.Artifact{Body: []byte(`{}`)}, domain.OutputContract{
|
|
ValidationMode: domain.ValidationJSONSchema,
|
|
SchemaPath: "%zz.json",
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "failed to register JSON schema") {
|
|
t.Fatalf("expected schema registration error, got result=%#v error=%v", res, err)
|
|
}
|
|
}
|
|
|
|
func TestFSValidatorJSONSchemaPathContainment(t *testing.T) {
|
|
t.Run("nested schema inside root succeeds", func(t *testing.T) {
|
|
v := NewFSValidator(fstest.MapFS{
|
|
"schemas/nested/events.schema.json": &fstest.MapFile{Data: []byte(`{
|
|
"type": "object",
|
|
"required": ["events"],
|
|
"properties": {
|
|
"events": {"type": "array"}
|
|
}
|
|
}`)},
|
|
}, "schemas")
|
|
|
|
res, err := v.Validate(context.Background(), &domain.Artifact{Body: []byte(`{"events":[]}`)}, domain.OutputContract{
|
|
ValidationMode: domain.ValidationJSONSchema,
|
|
SchemaPath: "nested/events.schema.json",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if res.Status != domain.ValidationPassed || !res.IsValid {
|
|
t.Fatalf("expected passed/valid, got status=%q valid=%v", res.Status, res.IsValid)
|
|
}
|
|
})
|
|
|
|
tests := []struct {
|
|
name string
|
|
schemaPath string
|
|
wantErr string
|
|
}{
|
|
{name: "parent escape rejected", schemaPath: "../outside.schema.json", wantErr: "escapes source root"},
|
|
{name: "absolute path rejected", schemaPath: "/outside.schema.json", wantErr: "must be relative"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
v := NewFSValidator(fstest.MapFS{
|
|
"schemas/events.schema.json": &fstest.MapFile{Data: []byte(`{"type":"object"}`)},
|
|
"outside.schema.json": &fstest.MapFile{Data: []byte(`{"type":"object"}`)},
|
|
"schemas/outside.schema.json": &fstest.MapFile{Data: []byte(`{"type":"object"}`)},
|
|
}, "schemas")
|
|
|
|
_, err := v.Validate(context.Background(), &domain.Artifact{Body: []byte(`{"events":[]}`)}, domain.OutputContract{
|
|
ValidationMode: domain.ValidationJSONSchema,
|
|
SchemaPath: tc.schemaPath,
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected schema path error")
|
|
}
|
|
if !strings.Contains(err.Error(), tc.wantErr) {
|
|
t.Fatalf("expected error to contain %q, got %v", tc.wantErr, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFSValidatorSingleSchemaFileUsesBaseName(t *testing.T) {
|
|
v := NewFSValidator(fstest.MapFS{
|
|
"events.schema.json": &fstest.MapFile{Data: []byte(`{
|
|
"type": "object",
|
|
"required": ["events"],
|
|
"properties": {
|
|
"events": {"type": "array"}
|
|
}
|
|
}`)},
|
|
}, "events.schema.json")
|
|
|
|
res, err := v.Validate(context.Background(), &domain.Artifact{Body: []byte(`{"events":[]}`)}, domain.OutputContract{
|
|
ValidationMode: domain.ValidationJSONSchema,
|
|
SchemaPath: "events.schema.json",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if res.Status != domain.ValidationPassed || !res.IsValid {
|
|
t.Fatalf("expected passed/valid, got status=%q valid=%v", res.Status, res.IsValid)
|
|
}
|
|
|
|
_, err = v.Validate(context.Background(), &domain.Artifact{Body: []byte(`{"events":[]}`)}, domain.OutputContract{
|
|
ValidationMode: domain.ValidationJSONSchema,
|
|
SchemaPath: "other.schema.json",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected schema path mismatch error")
|
|
}
|
|
}
|
|
|
|
func TestFSValidatorLoadSchemaDocument(t *testing.T) {
|
|
v := NewFSValidator(fstest.MapFS{
|
|
"schemas/schema.json": &fstest.MapFile{Data: []byte(`{"type":"object"}`)},
|
|
}, "schemas")
|
|
loader, ok := v.(SchemaDocumentLoader)
|
|
if !ok {
|
|
t.Fatal("fs validator must implement SchemaDocumentLoader")
|
|
}
|
|
|
|
doc, err := loader.LoadSchemaDocument(context.Background(), "schema.json")
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
obj, ok := doc.(map[string]any)
|
|
if !ok || obj["type"] != "object" {
|
|
t.Fatalf("unexpected schema document: %#v", doc)
|
|
}
|
|
}
|
|
|
|
func TestStandardValidatorJSONSchemaReferenceBoundaries(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(root, "child.json"), []byte(`{
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"type": "string"
|
|
}`), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
reference string
|
|
wantError string
|
|
writeOuter bool
|
|
}{
|
|
{name: "contained relative reference", reference: "child.json"},
|
|
{name: "remote reference", reference: "https://example.test/schema.json", wantError: "not a contained file reference"},
|
|
{name: "escaping reference", reference: "../outside.json", wantError: "escapes source root", writeOuter: true},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if tc.writeOuter {
|
|
if err := os.WriteFile(filepath.Join(filepath.Dir(root), "outside.json"), []byte(`{"type":"string"}`), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
schema := `{
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"$ref": ` + strconv.Quote(tc.reference) + `
|
|
}`
|
|
if err := os.WriteFile(filepath.Join(root, "root.json"), []byte(schema), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
v := NewStandardValidator(root)
|
|
result, err := v.Validate(context.Background(), &domain.Artifact{Body: []byte(`"value"`)}, domain.OutputContract{
|
|
ValidationMode: domain.ValidationJSONSchema,
|
|
SchemaPath: "root.json",
|
|
})
|
|
if tc.wantError == "" {
|
|
if err != nil || !result.IsValid {
|
|
t.Fatalf("expected contained reference to validate, got result=%#v error=%v", result, err)
|
|
}
|
|
return
|
|
}
|
|
if err == nil || !strings.Contains(err.Error(), tc.wantError) {
|
|
t.Fatalf("expected error containing %q, got %v", tc.wantError, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFSValidatorJSONSchemaReferenceBoundaries(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
reference string
|
|
wantError string
|
|
}{
|
|
{name: "same document fragment", reference: "#/$defs/value"},
|
|
{name: "contained relative reference", reference: "child.json"},
|
|
{name: "remote reference", reference: "https://example.test/schema.json", wantError: "is not allowed"},
|
|
{name: "escaping reference", reference: "../outside.json", wantError: "escapes source root"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
rootSchema := `{
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"$defs": {"value": {"type": "string"}},
|
|
"$ref": ` + strconv.Quote(tc.reference) + `
|
|
}`
|
|
v := NewFSValidator(fstest.MapFS{
|
|
"schemas/root.json": &fstest.MapFile{Data: []byte(rootSchema)},
|
|
"schemas/child.json": &fstest.MapFile{Data: []byte(`{"type":"string"}`)},
|
|
"outside.json": &fstest.MapFile{Data: []byte(`{"type":"string"}`)},
|
|
}, "schemas")
|
|
result, err := v.Validate(context.Background(), &domain.Artifact{Body: []byte(`"value"`)}, domain.OutputContract{
|
|
ValidationMode: domain.ValidationJSONSchema,
|
|
SchemaPath: "root.json",
|
|
})
|
|
if tc.wantError == "" {
|
|
if err != nil || !result.IsValid {
|
|
t.Fatalf("expected supported reference to validate, got result=%#v error=%v", result, err)
|
|
}
|
|
return
|
|
}
|
|
if err == nil || !strings.Contains(err.Error(), tc.wantError) {
|
|
t.Fatalf("expected error containing %q, got %v", tc.wantError, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestJSONSchemaDialectIsDraft2020(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
dialect string
|
|
wantError bool
|
|
}{
|
|
{name: "omitted uses supported default"},
|
|
{name: "draft 2020-12", dialect: "https://json-schema.org/draft/2020-12/schema"},
|
|
{name: "draft 7 rejected", dialect: "http://json-schema.org/draft-07/schema#", wantError: true},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
schema := map[string]any{"type": "object"}
|
|
if tc.dialect != "" {
|
|
schema["$schema"] = tc.dialect
|
|
}
|
|
data, err := json.Marshal(schema)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
v := NewFSValidator(fstest.MapFS{
|
|
"schema.json": &fstest.MapFile{Data: data},
|
|
}, ".")
|
|
_, err = v.Validate(context.Background(), &domain.Artifact{Body: []byte(`{}`)}, domain.OutputContract{
|
|
ValidationMode: domain.ValidationJSONSchema,
|
|
SchemaPath: "schema.json",
|
|
})
|
|
if tc.wantError {
|
|
if err == nil || !strings.Contains(err.Error(), "unsupported JSON Schema dialect") {
|
|
t.Fatalf("expected unsupported-dialect error, got %v", err)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("expected supported dialect, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func assertSchemaDocument(t *testing.T, got any, expectedJSON []byte) {
|
|
t.Helper()
|
|
|
|
var expected any
|
|
if err := json.Unmarshal(expectedJSON, &expected); err != nil {
|
|
t.Fatalf("decode expected schema document: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(got, expected) {
|
|
t.Fatalf("schema document mismatch:\n got: %#v\nwant: %#v", got, expected)
|
|
}
|
|
}
|