Files
ConfigKit/internal/engine/destination_tiers_test.go

329 lines
11 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 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]]
}