Files
ConfigKit/internal/engine/selected_overlay_test.go

277 lines
12 KiB
Go

package engine
import (
"errors"
"os"
"path/filepath"
"reflect"
"runtime"
"testing"
)
type overlayTestConfig struct {
Settings struct {
Kept string `yaml:"kept"`
Changed string `yaml:"changed"`
Added string `yaml:"added"`
} `yaml:"settings"`
Values []string `yaml:"values"`
Nullable *string `yaml:"nullable"`
Value any `yaml:"value"`
Schema map[string]string `yaml:"schema"`
}
func TestLoadAppliesSelectedOverlayAfterBaseAndBeforeDefaults(t *testing.T) {
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
imports: [{path: base.yml}]
settings:
kept: main
changed: base
values: [base, stale]
nullable: base
value: {child: base}
`)
basePath := writeImportFile(t, directory, "base.yml", "settings: {added: imported}\n")
overlayPath := writeImportFile(t, directory, "overlays/testing.yml", `settings:
changed: overlay
values: [overlay]
nullable: null
value: scalar
`)
result, err := testLoad[overlayTestConfig](File(mainPath), Options[overlayTestConfig]{
Imports: true,
Overlay: "testing",
Definition: Definition[overlayTestConfig]{Defaults: func() overlayTestConfig {
defaultValue := "default"
return overlayTestConfig{
Values: []string{"default"}, Nullable: &defaultValue,
Settings: struct {
Kept string `yaml:"kept"`
Changed string `yaml:"changed"`
Added string `yaml:"added"`
}{Kept: "default", Changed: "default", Added: "default"},
}
}},
})
if err != nil {
t.Fatal(err)
}
if result.Config.Settings.Kept != "main" || result.Config.Settings.Changed != "overlay" || result.Config.Settings.Added != "imported" {
t.Fatalf("settings = %#v", result.Config.Settings)
}
if !reflect.DeepEqual(result.Config.Values, []string{"overlay"}) || result.Config.Nullable != nil || result.Config.Value != "scalar" {
t.Fatalf("overlay values = values %#v nullable %#v value %#v", result.Config.Values, result.Config.Nullable, result.Config.Value)
}
sources := result.Sources()
wantPaths := []string{canonicalTestPath(t, mainPath), canonicalTestPath(t, basePath), canonicalTestPath(t, overlayPath)}
gotPaths := make([]string, len(sources))
for index, source := range sources {
gotPaths[index] = source.Path
}
if !reflect.DeepEqual(gotPaths, wantPaths) || sources[2].Layer != LayerOverlay {
t.Fatalf("sources = %#v", sources)
}
changed, _ := result.Metadata().Lookup(Keys("settings", "changed"))
kept, _ := result.Metadata().Lookup(Keys("settings", "kept"))
if changed.Origin.Layer != LayerOverlay || changed.Origin.Locations[0].SourceID != "source-3" || kept.Origin.Layer != LayerBase {
t.Fatalf("origins = changed %#v kept %#v", changed, kept)
}
if _, ok := result.Metadata().Lookup(Keys("value", "child")); ok {
t.Fatal("removed child remains effective")
}
childHistory := result.Metadata().History(Keys("value", "child"))
if len(childHistory) < 2 || childHistory[len(childHistory)-1].Path.String() != "/value" || childHistory[len(childHistory)-1].Origin.Layer != LayerOverlay {
t.Fatalf("removed child history = %#v", childHistory)
}
if _, ok := result.Metadata().Lookup(Keys("values").Index(1)); ok {
t.Fatal("removed sequence index remains effective")
}
indexHistory := result.Metadata().History(Keys("values").Index(1))
if len(indexHistory) < 2 || indexHistory[len(indexHistory)-1].Path.String() != "/values" {
t.Fatalf("removed sequence history = %#v", indexHistory)
}
}
func TestLoadOverlaySelectionUsesOneFixedYAMLFile(t *testing.T) {
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "settings: {kept: base}\n")
writeImportFile(t, directory, "overlays/testing.yml", "settings: {changed: selected}\n")
writeImportFile(t, directory, "profiles/testing.yml", "settings: {changed: legacy}\n")
writeImportFile(t, directory, "overlays/default.yml", "settings: {changed: forbidden}\n")
for _, name := range []string{"", "default"} {
result, err := testLoad[overlayTestConfig](File(mainPath), Options[overlayTestConfig]{Overlay: name})
if err != nil || result.Config.Settings.Changed != "" || len(result.Sources()) != 1 {
t.Fatalf("inactive overlay %q = %#v, %v", name, result, err)
}
}
selected, err := testLoad[overlayTestConfig](File(mainPath), Options[overlayTestConfig]{Overlay: "testing"})
if err != nil || selected.Config.Settings.Changed != "selected" || len(selected.Sources()) != 2 ||
selected.Sources()[1].Label != "overlays/testing.yml" || selected.Sources()[1].Layer != LayerOverlay {
t.Fatalf("selected overlay = %#v, %v", selected, err)
}
t.Run("legacy directory is not selected", func(t *testing.T) {
legacyDirectory := t.TempDir()
legacyMain := writeImportFile(t, legacyDirectory, "main.yml", "settings: {kept: base}\n")
writeImportFile(t, legacyDirectory, "profiles/testing.yml", "settings: {changed: legacy}\n")
requireOverlayLoadCode(t, File(legacyMain), Options[overlayTestConfig]{Overlay: "testing"}, CodeOverlayMissing)
})
for _, name := range []string{"../testing", "testing.yml", "one,two", "with space", "é", "bad\x00name"} {
t.Run(name, func(t *testing.T) {
requireOverlayLoadCode(t, File(mainPath), Options[overlayTestConfig]{Overlay: name}, CodeOptionsInvalid)
})
}
requireOverlayLoadCode(t, File(mainPath), Options[overlayTestConfig]{Overlay: "missing"}, CodeOverlayMissing)
writeImportFile(t, directory, "overlays/fallback.yaml", "settings: {changed: fallback}\n")
requireOverlayLoadCode(t, File(mainPath), Options[overlayTestConfig]{Overlay: "fallback"}, CodeOverlayMissing)
}
func TestLoadOverlayRejectsDirectivesVersionsAndBaseAliases(t *testing.T) {
versionRule := &VersionRule{Path: Keys("schema", "version"), Check: func(version Version, _ *Validation) error {
if !version.Present || version.Text != "4" {
return errors.New("unexpected version")
}
return nil
}}
for _, test := range []struct {
name string
overlay string
code Code
}{
{name: "root directive", overlay: "configkit: {}\n", code: CodeOverlayDirective},
{name: "nested directive", overlay: "settings: {configkit: {}}\n", code: CodeOverlayDirective},
{name: "equal integer version", overlay: "schema: {version: 4}\n", code: CodeOverlayVersion},
{name: "ancestor scalar", overlay: "schema: null\n", code: CodeOverlayVersion},
} {
t.Run(test.name, func(t *testing.T) {
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "schema: {version: 4}\n")
writeImportFile(t, directory, "overlays/testing.yml", test.overlay)
requireOverlayLoadCode(t, File(mainPath), Options[overlayTestConfig]{Overlay: "testing", Definition: Definition[overlayTestConfig]{Version: versionRule}}, test.code)
})
}
t.Run("mapping sibling is allowed", func(t *testing.T) {
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "schema: {version: 4}\n")
writeImportFile(t, directory, "overlays/testing.yml", "schema: {other: allowed}\n")
result, err := testLoad[overlayTestConfig](File(mainPath), Options[overlayTestConfig]{Overlay: "testing", Definition: Definition[overlayTestConfig]{Version: versionRule}})
if err != nil || result.Config.Schema["other"] != "allowed" {
t.Fatalf("mapping sibling = %#v, %v", result, err)
}
})
t.Run("overlay aliases imported base", func(t *testing.T) {
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: overlays/testing.yml}]}\n")
writeImportFile(t, directory, "overlays/testing.yml", "settings: {changed: value}\n")
requireOverlayLoadCode(t, File(mainPath), Options[overlayTestConfig]{Imports: true, Overlay: "testing"}, CodeOverlayRepeated)
})
}
func TestLoadOverlayKeepsBaseFailuresAndFilesystemContainment(t *testing.T) {
t.Run("base parse precedes overlay", func(t *testing.T) {
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "settings: [\n")
writeImportFile(t, directory, "overlays/testing.yml", "settings: {changed: overlay}\n")
requireOverlayLoadCode(t, File(mainPath), Options[overlayTestConfig]{Overlay: "testing"}, CodeYAMLInvalid)
})
t.Run("overlay parse", func(t *testing.T) {
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "{}\n")
writeImportFile(t, directory, "overlays/testing.yml", "settings: [\n")
loadError := requireOverlayLoadCode(t, File(mainPath), Options[overlayTestConfig]{Overlay: "testing"}, CodeYAMLInvalid)
if loadError.Diagnostics()[0].Primary.SourceID != "source-2" || len(loadError.Sources()) != 2 {
t.Fatalf("overlay parse attribution = %#v / %#v", loadError.Diagnostics(), loadError.Sources())
}
})
if runtime.GOOS == "linux" || runtime.GOOS == "darwin" {
t.Run("symlink escape", func(t *testing.T) {
root := t.TempDir()
outside := t.TempDir()
outsideOverlay := writeImportFile(t, outside, "testing.yml", "settings: {changed: outside}\n")
if err := os.Mkdir(filepath.Join(root, "overlays"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.Symlink(outsideOverlay, filepath.Join(root, "overlays/testing.yml")); err != nil {
t.Skipf("symlinks unavailable: %v", err)
}
mainPath := writeImportFile(t, root, "main.yml", "{}\n")
requireOverlayLoadCode(t, File(mainPath), Options[overlayTestConfig]{Overlay: "testing"}, CodeOverlayOutsideRoot)
})
}
t.Run("byte source context", func(t *testing.T) {
directory := t.TempDir()
writeImportFile(t, directory, "overlays/testing.yml", "settings: {changed: selected}\n")
result, err := testLoad[overlayTestConfig](Bytes("s3://bucket/main.yml", []byte("{}\n")), Options[overlayTestConfig]{
Overlay: "testing", Filesystem: &FilesystemContext{BaseDir: directory, Root: directory},
})
if err != nil || result.Config.Settings.Changed != "selected" {
t.Fatalf("byte overlay = %#v, %v", result, err)
}
requireOverlayLoadCode(t, Bytes("memory.yml", []byte("{}\n")), Options[overlayTestConfig]{Overlay: "testing"}, CodeOptionsInvalid)
})
}
func TestLoadOverlayCountsAgainstAggregateBudgets(t *testing.T) {
for _, test := range []struct {
name string
limits func(parsedSource, int) importLimits
}{
{name: "source count", limits: func(_ parsedSource, _ int) importLimits {
return importLimits{sources: 1, bytes: maxTotalBytes, nodes: maxDocumentNodes}
}},
{name: "aggregate bytes", limits: func(main parsedSource, overlayBytes int) importLimits {
return importLimits{sources: 2, bytes: main.bytes + int64(overlayBytes) - 1, nodes: maxDocumentNodes}
}},
{name: "aggregate nodes", limits: func(main parsedSource, _ int) importLimits {
return importLimits{sources: 2, bytes: maxTotalBytes, nodes: countInputNodes(main.document) + 1}
}},
} {
t.Run(test.name, func(t *testing.T) {
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "{}\n")
overlayData := "settings: {changed: selected}\n"
writeImportFile(t, directory, "overlays/testing.yml", overlayData)
main, diagnostics := parseSource(File(mainPath), "source-1", defaultParseLimits())
if len(diagnostics) != 0 {
t.Fatal(diagnostics)
}
base, diagnostics := composeDocuments(main.document)
if len(diagnostics) != 0 {
t.Fatal(diagnostics)
}
base.inputBytes = main.bytes
base.inputNodes = countInputNodes(main.document)
_, _, diagnostics = loadSelectedOverlayDocument(File(mainPath), nil, main, base, sourceInfos(main.source), "testing", nil, test.limits(main, len(overlayData)))
if len(diagnostics) == 0 || diagnostics[0].Code != CodeOverlayLimitExceeded {
t.Fatalf("diagnostics = %#v", diagnostics)
}
})
}
}
func requireOverlayLoadCode(t *testing.T, source Source, options Options[overlayTestConfig], code Code) *LoadError {
t.Helper()
result, err := testLoad[overlayTestConfig](source, options)
if result != nil || err == nil {
t.Fatalf("result, error = %#v, %v", result, err)
}
var loadError *LoadError
if !errors.As(err, &loadError) {
t.Fatalf("error type = %T", err)
}
diagnostics := loadError.Diagnostics()
if len(diagnostics) == 0 || diagnostics[0].Code != code {
t.Fatalf("diagnostics = %#v, want %s", diagnostics, code)
}
return loadError
}