348 lines
12 KiB
Go
348 lines
12 KiB
Go
package engine
|
|
|
|
import (
|
|
"path/filepath"
|
|
"reflect"
|
|
"testing"
|
|
"testing/fstest"
|
|
)
|
|
|
|
func TestLayeredDestinationStrictCompositionBoundaries(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
inputs []Input
|
|
wantPath []string
|
|
want any
|
|
alsoPath []string
|
|
alsoWant any
|
|
}{
|
|
{
|
|
name: "disjoint peers at one destination",
|
|
inputs: []Input{
|
|
{Source: Bytes("left", []byte("left: one\n")), Destination: Keys("application")},
|
|
{Source: Bytes("right", []byte("right: two\n")), Destination: Keys("application")},
|
|
},
|
|
wantPath: []string{"application", "right"},
|
|
want: "two",
|
|
alsoPath: []string{"application", "left"},
|
|
alsoWant: "one",
|
|
},
|
|
{
|
|
name: "different destinations",
|
|
inputs: []Input{
|
|
{Source: Bytes("left", []byte("value: shared\n")), Destination: Keys("left")},
|
|
{Source: Bytes("right", []byte("value: shared\n")), Destination: Keys("right")},
|
|
},
|
|
wantPath: []string{"right", "value"},
|
|
want: "shared",
|
|
},
|
|
{
|
|
name: "ancestor and descendant destinations",
|
|
inputs: []Input{
|
|
{Source: Bytes("parent", []byte("branch: {base: one}\n")), Destination: Keys("application")},
|
|
{Source: Bytes("child", []byte("leaf: two\n")), Destination: Keys("application", "branch")},
|
|
},
|
|
wantPath: []string{"application", "branch", "leaf"},
|
|
want: "two",
|
|
alsoPath: []string{"application", "branch", "base"},
|
|
alsoWant: "one",
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
loaded, err := LoadLayered(Input{Source: NoSource()}, testSecretBinding(), LayeredOptions[map[string]any]{
|
|
Underlays: []Underlay{{Inputs: test.inputs}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("LoadLayered: %v", err)
|
|
}
|
|
result := testLoaded(loaded)
|
|
if got := layeredMapValue(t, result.Config, test.wantPath...); got != test.want {
|
|
t.Fatalf("value at %v = %#v, want %#v", test.wantPath, got, test.want)
|
|
}
|
|
if len(test.alsoPath) != 0 {
|
|
if got := layeredMapValue(t, result.Config, test.alsoPath...); got != test.alsoWant {
|
|
t.Fatalf("value at %v = %#v, want %#v", test.alsoPath, got, test.alsoWant)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
left string
|
|
right string
|
|
}{
|
|
{name: "equal values conflict", left: "value: same\n", right: "value: same\n"},
|
|
{name: "unequal values conflict", left: "value: left\n", right: "value: right\n"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
inputs := []Input{
|
|
{Source: Bytes("left", []byte(test.left)), Destination: Keys("application")},
|
|
{Source: Bytes("right", []byte(test.right)), Destination: Keys("application")},
|
|
}
|
|
loaded, err := LoadLayered(Input{Source: NoSource()}, testSecretBinding(), LayeredOptions[map[string]any]{
|
|
Underlays: []Underlay{{Inputs: inputs}},
|
|
})
|
|
if loaded != nil || groupedErrorCode(t, err) != CodeComposeConflict {
|
|
t.Fatalf("strict composition result = %#v, %v", loaded, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("strict conflict is not hidden by an upper override", func(t *testing.T) {
|
|
inputs := []Input{
|
|
{Source: Bytes("left", []byte("value: left\n")), Destination: Keys("application")},
|
|
{Source: Bytes("right", []byte("value: right\n")), Destination: Keys("application")},
|
|
}
|
|
loaded, err := LoadLayered(
|
|
Input{Source: Bytes("main", []byte("value: upper\n")), Destination: Keys("application")},
|
|
testSecretBinding(),
|
|
LayeredOptions[map[string]any]{Underlays: []Underlay{{Inputs: inputs}}},
|
|
)
|
|
if loaded != nil || groupedErrorCode(t, err) != CodeComposeConflict {
|
|
t.Fatalf("strict composition result = %#v, %v", loaded, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestLayeredDestinationMatchesExplicitlyWrappedInput(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
load := func(input Input) map[string]any {
|
|
t.Helper()
|
|
loaded, err := LoadLayered(Input{Source: NoSource()}, testSecretBinding(), LayeredOptions[map[string]any]{
|
|
Underlays: []Underlay{{Inputs: []Input{input}}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("LoadLayered: %v", err)
|
|
}
|
|
return testLoaded(loaded).Config
|
|
}
|
|
|
|
placed := load(Input{Source: Bytes("placed", []byte("field: value\n")), Destination: Keys("application")})
|
|
authored := load(Input{Source: Bytes("authored", []byte("application: {field: value}\n"))})
|
|
if !reflect.DeepEqual(placed, authored) {
|
|
t.Fatalf("placed configuration %#v differs from explicitly wrapped %#v", placed, authored)
|
|
}
|
|
}
|
|
|
|
func TestEmptyMountedMappingRetainsLowerChildren(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
destination := Keys("collections", "weather")
|
|
loaded, err := LoadLayered(
|
|
Input{Source: Bytes("empty-main", []byte("{}\n")), Destination: destination},
|
|
testSecretBinding(),
|
|
LayeredOptions[map[string]any]{Underlays: []Underlay{{Inputs: []Input{{
|
|
Source: Bytes("lower", []byte("prompt: {title: retained}\n")), Destination: destination,
|
|
}}}}},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("LoadLayered: %v", err)
|
|
}
|
|
if got := layeredMapValue(t, testLoaded(loaded).Config, "collections", "weather", "prompt", "title"); got != "retained" {
|
|
t.Fatalf("lower child beneath empty mounted mapping = %#v, want retained", got)
|
|
}
|
|
}
|
|
|
|
func TestLayeredDestinationOverlayRetainsOrdinaryValueSemantics(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
filesystem := NewFilesystem("bundle", fstest.MapFS{
|
|
"overlays/test.yml": &fstest.MapFile{Data: []byte("settings:\n metadata:\n version: overlay\n")},
|
|
})
|
|
loaded, err := LoadLayered(
|
|
Input{Source: Bytes("main", []byte("mapping: {shared: main}\nsequence: [9]\ncleared: null\nenabled: false\nmetadata: {version: main}\n")), Destination: Keys("settings")},
|
|
testSecretBinding(),
|
|
LayeredOptions[map[string]any]{
|
|
Underlays: []Underlay{{Inputs: []Input{{
|
|
Source: Bytes("underlay", []byte("mapping: {retained: lower, shared: lower}\nsequence: [1, 2]\ncleared: {old: true}\nenabled: true\ncount: 7\nmetadata: {version: lower, label: retained}\n")),
|
|
Destination: Keys("settings"),
|
|
}}}},
|
|
Overlay: "test",
|
|
OverlayFilesystem: &FilesystemContext{Root: ".", BaseDir: ".", FS: &filesystem},
|
|
Overrides: []Override{{Path: Keys("settings", "count"), Value: 0, Flag: "--count"}},
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("LoadLayered: %v", err)
|
|
}
|
|
result := testLoaded(loaded)
|
|
settings := layeredMap(t, result.Config, "settings")
|
|
mapping := layeredMap(t, settings, "mapping")
|
|
if mapping["retained"] != "lower" || mapping["shared"] != "main" {
|
|
t.Fatalf("recursive mapping overlay = %#v", mapping)
|
|
}
|
|
sequence, ok := settings["sequence"].([]any)
|
|
if !ok || len(sequence) != 1 || sequence[0] != int64(9) {
|
|
t.Fatalf("sequence overlay = %#v", settings["sequence"])
|
|
}
|
|
if value, present := settings["cleared"]; !present || value != nil {
|
|
t.Fatalf("explicit null = %#v, present %v", value, present)
|
|
}
|
|
if value, ok := settings["enabled"].(bool); !ok || value {
|
|
t.Fatalf("explicit false = %#v", settings["enabled"])
|
|
}
|
|
if value, ok := settings["count"].(int64); !ok || value != 0 {
|
|
t.Fatalf("explicit zero override = %#v", settings["count"])
|
|
}
|
|
metadata := layeredMap(t, settings, "metadata")
|
|
if metadata["version"] != "overlay" || metadata["label"] != "retained" {
|
|
t.Fatalf("version-only overlay = %#v", metadata)
|
|
}
|
|
if _, present := settings["settings"]; present {
|
|
t.Fatalf("upper tiers were incorrectly nested under the main destination: %#v", result.Config)
|
|
}
|
|
}
|
|
|
|
func TestLayeredDestinationChecksVersionOnceOnComposedMain(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type appConfig struct {
|
|
Version int8 `yaml:"version"`
|
|
Count int `yaml:"count"`
|
|
Main string `yaml:"main"`
|
|
Imported string `yaml:"imported"`
|
|
Lower string `yaml:"lower"`
|
|
}
|
|
type config struct {
|
|
App appConfig `yaml:"app"`
|
|
}
|
|
filesystem := NewFilesystem("bundle", fstest.MapFS{
|
|
"main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: fragment.yml}]}\nversion: 6\ncount: 4\nmain: main\n")},
|
|
"fragment.yml": &fstest.MapFile{Data: []byte("imported: included\n")},
|
|
})
|
|
checks := 0
|
|
loaded, err := LoadLayered(
|
|
Input{Source: filesystem.File("main.yml"), Imports: true, Destination: Keys("app")},
|
|
testSecretBinding(),
|
|
LayeredOptions[config]{
|
|
Definition: Definition[config]{Version: &VersionRule{Path: Keys("app", "version"), Check: func(version Version, _ *Validation) error {
|
|
checks++
|
|
if !version.Present || version.Text != "6" {
|
|
t.Errorf("nested main version = %#v", version)
|
|
}
|
|
return nil
|
|
}}},
|
|
Underlays: []Underlay{{Inputs: []Input{{
|
|
Source: Bytes("lower", []byte("lower: underlay\n")), Destination: Keys("app"),
|
|
}}}},
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("LoadLayered: %v", err)
|
|
}
|
|
result := testLoaded(loaded)
|
|
if checks != 1 {
|
|
t.Fatalf("version hook calls = %d, want 1", checks)
|
|
}
|
|
if result.Config.App.Version != 6 || result.Config.App.Count != 4 || result.Config.App.Main != "main" || result.Config.App.Imported != "included" || result.Config.App.Lower != "underlay" {
|
|
t.Fatalf("nested app = %#v", result.Config.App)
|
|
}
|
|
}
|
|
|
|
func TestLayeredDestinationVersionClaimsUsePlacedApplicationPaths(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rule := &VersionRule{Path: Keys("app", "schema", "version"), Check: func(Version, *Validation) error { return nil }}
|
|
for _, test := range []struct {
|
|
name string
|
|
body string
|
|
wantCode Code
|
|
wantValue any
|
|
}{
|
|
{name: "underlay direct claim", body: "schema: {version: v2}\n", wantCode: CodeUnderlayVersion},
|
|
{name: "underlay ancestor claim", body: "schema: null\n", wantCode: CodeUnderlayVersion},
|
|
{name: "underlay mapping ancestor without claim", body: "schema: {name: ordinary}\n", wantValue: "ordinary"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
loaded, err := LoadLayered(
|
|
Input{Source: NoSource()},
|
|
testSecretBinding(),
|
|
LayeredOptions[map[string]any]{
|
|
Definition: Definition[map[string]any]{Version: rule},
|
|
Underlays: []Underlay{{Inputs: []Input{{
|
|
Source: Bytes("underlay", []byte(test.body)), Destination: Keys("app"),
|
|
}}}},
|
|
},
|
|
)
|
|
if test.wantCode != "" {
|
|
if loaded != nil || groupedErrorCode(t, err) != test.wantCode {
|
|
t.Fatalf("version claim result = %#v, %v", loaded, err)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("nonclaiming mapping ancestor was rejected: %v", err)
|
|
}
|
|
result := testLoaded(loaded)
|
|
if got := layeredMapValue(t, result.Config, "app", "schema", "name"); got != test.wantValue {
|
|
t.Fatalf("nested ordinary value = %#v", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
body string
|
|
wantCode Code
|
|
}{
|
|
{name: "imported direct claim", body: "schema: {version: v2}\n", wantCode: CodeImportVersion},
|
|
{name: "imported ancestor claim", body: "schema: null\n", wantCode: CodeImportVersion},
|
|
{name: "imported mapping ancestor without claim", body: "schema: {name: ordinary}\n"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
directory := t.TempDir()
|
|
writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: fragment.yml}]}\n")
|
|
writeImportFile(t, directory, "fragment.yml", test.body)
|
|
loaded, err := LoadLayered(
|
|
Input{Source: NoSource()},
|
|
testSecretBinding(),
|
|
LayeredOptions[map[string]any]{
|
|
Definition: Definition[map[string]any]{Version: rule},
|
|
Underlays: []Underlay{{Inputs: []Input{{
|
|
Source: File(filepath.Join(directory, "main.yml")), Imports: true, Destination: Keys("app"),
|
|
}}}},
|
|
},
|
|
)
|
|
if test.wantCode != "" {
|
|
if loaded != nil || groupedErrorCode(t, err) != test.wantCode {
|
|
t.Fatalf("version claim result = %#v, %v", loaded, err)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("nonclaiming imported mapping ancestor was rejected: %v", err)
|
|
}
|
|
result := testLoaded(loaded)
|
|
if got := layeredMapValue(t, result.Config, "app", "schema", "name"); got != "ordinary" {
|
|
t.Fatalf("nested ordinary value = %#v", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func layeredMap(t *testing.T, value map[string]any, key string) map[string]any {
|
|
t.Helper()
|
|
result, ok := value[key].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("%q = %#v, want mapping", key, value[key])
|
|
}
|
|
return result
|
|
}
|
|
|
|
func layeredMapValue(t *testing.T, value map[string]any, keys ...string) any {
|
|
t.Helper()
|
|
for _, key := range keys[:len(keys)-1] {
|
|
value = layeredMap(t, value, key)
|
|
}
|
|
return value[keys[len(keys)-1]]
|
|
}
|