215 lines
7.8 KiB
Go
215 lines
7.8 KiB
Go
package engine
|
|
|
|
import (
|
|
"errors"
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
func TestValidationCollectsSchemaAwareReportsAndNearestLocations(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type config struct {
|
|
Primary string `yaml:"primary"`
|
|
Missing string `yaml:"missing"`
|
|
Dynamic map[string]any `yaml:"dynamic"`
|
|
}
|
|
result, err := Load(Bytes("validation.yml", []byte("primary: configured\ndynamic: {present: 1}\n")), Options[config]{Definition: Definition[config]{
|
|
Validate: func(_ *config, validation *Validation) {
|
|
validation.Error(Keys("missing"), "is required", Keys("primary"))
|
|
validation.Error(Keys("dynamic", "missing"), "dynamic entry is required")
|
|
validation.Warning(Keys("primary"), "primary value is deprecated")
|
|
},
|
|
}})
|
|
if result != nil || err == nil {
|
|
t.Fatalf("result, err = %#v, %v", result, err)
|
|
}
|
|
var loadError *LoadError
|
|
if !errors.As(err, &loadError) {
|
|
t.Fatalf("error type = %T", err)
|
|
}
|
|
diagnostics := loadError.Diagnostics()
|
|
if len(diagnostics) != 3 {
|
|
t.Fatalf("diagnostics = %#v", diagnostics)
|
|
}
|
|
byPath := make(map[string]Diagnostic)
|
|
for _, diagnostic := range diagnostics {
|
|
byPath[diagnostic.Path.String()] = diagnostic
|
|
}
|
|
missing := byPath["/missing"]
|
|
if missing.Code != CodeValidateError || missing.Primary == nil || missing.Primary.Line != 1 || len(missing.Related) != 1 || missing.Related[0].Line != 1 {
|
|
t.Fatalf("missing diagnostic = %#v", missing)
|
|
}
|
|
dynamic := byPath["/dynamic/missing"]
|
|
if dynamic.Code != CodeValidateError || dynamic.Primary == nil || dynamic.Primary.Line != 2 {
|
|
t.Fatalf("dynamic diagnostic = %#v", dynamic)
|
|
}
|
|
if warning := byPath["/primary"]; warning.Code != CodeValidateWarning || warning.Severity != SeverityWarning {
|
|
t.Fatalf("warning = %#v", warning)
|
|
}
|
|
if len(loadError.Sources()) != 1 {
|
|
t.Fatalf("sources = %#v", loadError.Sources())
|
|
}
|
|
}
|
|
|
|
func TestValidationRejectsUnknownStructPaths(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type config struct {
|
|
Known string `yaml:"known"`
|
|
}
|
|
result, err := Load(Bytes("validation-path.yml", []byte("known: value\n")), Options[config]{Definition: Definition[config]{
|
|
Validate: func(_ *config, validation *Validation) {
|
|
validation.Error(Keys("unknown"), "must be configured")
|
|
},
|
|
}})
|
|
assertLoadDiagnostic(t, result, err, CodeValidateInvalidPath)
|
|
}
|
|
|
|
func TestValidationMutationFailsWithoutPublishingResult(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type config struct {
|
|
Value int `yaml:"value"`
|
|
Items map[string]int `yaml:"items"`
|
|
}
|
|
result, err := Load(Bytes("mutation.yml", []byte("value: 1\nitems: {one: 1}\n")), Options[config]{Definition: Definition[config]{
|
|
Validate: func(configuration *config, _ *Validation) {
|
|
configuration.Value = 2
|
|
configuration.Items["one"] = 2
|
|
},
|
|
}})
|
|
if result != nil || err == nil {
|
|
t.Fatalf("result, err = %#v, %v", result, err)
|
|
}
|
|
var loadError *LoadError
|
|
if !errors.As(err, &loadError) {
|
|
t.Fatal(err)
|
|
}
|
|
var paths []string
|
|
for _, diagnostic := range loadError.Diagnostics() {
|
|
if diagnostic.Code != CodeValidateMutation || diagnostic.Message != "application validation mutated the configuration" {
|
|
t.Fatalf("diagnostic = %#v", diagnostic)
|
|
}
|
|
paths = append(paths, diagnostic.Path.String())
|
|
}
|
|
if !reflect.DeepEqual(paths, []string{"/items/one", "/value"}) {
|
|
t.Fatalf("mutation paths = %#v", paths)
|
|
}
|
|
}
|
|
|
|
func TestHookOrderStopsAfterErrorsAndContinuesAfterWarnings(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type config struct {
|
|
Version string `yaml:"version"`
|
|
Value int `yaml:"value"`
|
|
}
|
|
t.Run("warnings continue", func(t *testing.T) {
|
|
var events []string
|
|
definition := Definition[config]{
|
|
Version: &VersionRule{Path: Keys("version"), Check: func(_ Version, validation *Validation) error {
|
|
events = append(events, "version")
|
|
validation.Warning(Keys("version"), "version warning")
|
|
return nil
|
|
}},
|
|
Defaults: func() config {
|
|
events = append(events, "defaults")
|
|
return config{Value: 1}
|
|
},
|
|
Resolve: func(_ *config, resolution *Resolution) error {
|
|
events = append(events, "resolve")
|
|
resolution.Warning(Keys("value"), "resolution warning")
|
|
return nil
|
|
},
|
|
Validate: func(_ *config, validation *Validation) {
|
|
events = append(events, "validate")
|
|
validation.Warning(Keys("value"), "validation warning")
|
|
},
|
|
}
|
|
result, err := Load(Bytes("order.yml", []byte("version: one\n")), Options[config]{Definition: definition})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(events, []string{"version", "defaults", "resolve", "validate"}) || len(result.Diagnostics()) != 3 {
|
|
t.Fatalf("events = %#v, diagnostics = %#v", events, result.Diagnostics())
|
|
}
|
|
})
|
|
|
|
t.Run("version error stops later hooks", func(t *testing.T) {
|
|
var events []string
|
|
definition := Definition[config]{
|
|
Version: &VersionRule{Path: Keys("version"), Check: func(_ Version, validation *Validation) error {
|
|
events = append(events, "version")
|
|
validation.Error(Keys("version"), "unsupported version")
|
|
return nil
|
|
}},
|
|
Defaults: func() config { events = append(events, "defaults"); return config{} },
|
|
Resolve: func(_ *config, _ *Resolution) error { events = append(events, "resolve"); return nil },
|
|
Validate: func(_ *config, _ *Validation) { events = append(events, "validate") },
|
|
}
|
|
result, err := Load(Bytes("order-error.yml", []byte("version: one\n")), Options[config]{Definition: definition})
|
|
assertLoadDiagnostic(t, result, err, CodeVersionError)
|
|
if !reflect.DeepEqual(events, []string{"version"}) {
|
|
t.Fatalf("events = %#v", events)
|
|
}
|
|
})
|
|
|
|
t.Run("resolution error stops validation", func(t *testing.T) {
|
|
var validated bool
|
|
result, err := Load(Bytes("resolution-order.yml", []byte("value: 1\nversion: one\n")), Options[config]{Definition: Definition[config]{
|
|
Resolve: func(_ *config, resolution *Resolution) error {
|
|
resolution.Error(Keys("value"), "cannot resolve")
|
|
return nil
|
|
},
|
|
Validate: func(_ *config, _ *Validation) { validated = true },
|
|
}})
|
|
assertLoadDiagnostic(t, result, err, CodeResolveError)
|
|
if validated {
|
|
t.Fatal("validation ran after resolution error")
|
|
}
|
|
})
|
|
|
|
t.Run("decode error stops application hooks", func(t *testing.T) {
|
|
var resolved, validated bool
|
|
result, err := Load(Bytes("decode-order.yml", []byte("value: wrong\nversion: one\n")), Options[config]{Definition: Definition[config]{
|
|
Resolve: func(_ *config, _ *Resolution) error { resolved = true; return nil },
|
|
Validate: func(_ *config, _ *Validation) { validated = true },
|
|
}})
|
|
assertLoadDiagnostic(t, result, err, CodeDecodeType)
|
|
if resolved || validated {
|
|
t.Fatalf("later hooks ran: resolve=%v validate=%v", resolved, validated)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCompleteSingleSourceLifecycleKeepsMetadataAgreement(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type config struct {
|
|
Input int `yaml:"input"`
|
|
Derived int `yaml:"derived"`
|
|
}
|
|
result, err := Load(Bytes("complete.yml", []byte("input: 3\n")), Options[config]{Definition: Definition[config]{
|
|
Defaults: func() config { return config{Derived: 1} },
|
|
Resolve: func(configuration *config, resolution *Resolution) error {
|
|
Default(resolution, Keys("derived"), &configuration.Derived, configuration.Input*2, "derived from input", Keys("input"))
|
|
return nil
|
|
},
|
|
Validate: func(configuration *config, validation *Validation) {
|
|
if configuration.Derived != 6 {
|
|
validation.Error(Keys("derived"), "derived value is inconsistent", Keys("input"))
|
|
}
|
|
},
|
|
}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
input, _ := result.Metadata().Lookup(Keys("input"))
|
|
derived, _ := result.Metadata().Lookup(Keys("derived"))
|
|
history := result.Metadata().History(Keys("derived"))
|
|
if result.Config != (config{Input: 3, Derived: 6}) || !input.Explicit || input.Origin.Layer != LayerBase || derived.Explicit || derived.Origin.Operation != ChangeDerivedDefault || history[len(history)-1].Dependencies[0].String() != "/input" {
|
|
t.Fatalf("config/metadata = %#v / %#v / %#v / %#v", result.Config, input, derived, history)
|
|
}
|
|
}
|