package validate import ( "context" "os" "path/filepath" "strings" "testing" "testing/fstest" "gitea.maximumdirect.net/eric/scriptorium/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 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 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 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) } }