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, "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 { t.Fatalf("selected overlay = %#v, %v", selected, err) } 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 }