package configkit_test import ( "os" "path/filepath" "testing" "testing/fstest" configkit "go.maximumdirect.net/configkit" ) func TestMainRelativeCompanion(t *testing.T) { dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "main.yml"), []byte("count: 3\n"), 0600); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "extra.yml"), []byte("settings: {companion: host}\n"), 0600); err != nil { t.Fatal(err) } binding := configkit.MainRelative() companion := configkit.Input{Source: configkit.Bytes("companions", []byte("config: {imports: [{path: extra.yml}]}\n")), Imports: true, MainRelative: binding} result, err := configkit.LoadLayered(configkit.Input{Source: configkit.File(filepath.Join(dir, "main.yml"))}, configkit.LayeredOptions[layeredFacadeConfig]{Underlays: []configkit.Underlay{{Inputs: []configkit.Input{companion}}}}) if err != nil { t.Fatalf("%v: %#v", err, err.(*configkit.LoadError).Diagnostics()) } if result.Config.Count != 3 || result.Config.Settings["companion"] != "host" { t.Fatalf("configuration = %#v", result.Config) } native := configkit.NewFilesystem("bundle", fstest.MapFS{"dir/main.yml": &fstest.MapFile{Data: []byte("count: 4\n")}, "dir/extra.yml": &fstest.MapFile{Data: []byte("settings: {companion: native}\n")}}) result, err = configkit.LoadLayered(configkit.Input{Source: native.File("dir/main.yml")}, configkit.LayeredOptions[layeredFacadeConfig]{Underlays: []configkit.Underlay{{Inputs: []configkit.Input{companion}}}}) if err != nil { t.Fatalf("%v: %#v", err, err.(*configkit.LoadError).Diagnostics()) } if result.Config.Count != 4 || result.Config.Settings["companion"] != "native" { t.Fatalf("configuration = %#v", result.Config) } } func TestMainRelativeContextSelection(t *testing.T) { root := t.TempDir() base := filepath.Join(root, "main") if err := os.Mkdir(base, 0700); err != nil { t.Fatal(err) } for path, value := range map[string]string{"main/main.yml": "count: 1\n", "main/extra.yml": "settings: {value: derived}\n", "extra.yml": "settings: {value: fallback}\n"} { if err := os.WriteFile(filepath.Join(root, path), []byte(value), 0600); err != nil { t.Fatal(err) } } link := filepath.Join(root, "link.yml") if err := os.Symlink(filepath.Join(base, "main.yml"), link); err != nil { t.Fatal(err) } companion := configkit.Input{Source: configkit.Bytes("companion", []byte("config: {imports: [{path: extra.yml}]}\n")), Imports: true, MainRelative: &configkit.MainRelativeContext{Fallback: &configkit.FilesystemContext{Root: root, BaseDir: root}}} opts := configkit.LayeredOptions[layeredFacadeConfig]{Underlays: []configkit.Underlay{{Inputs: []configkit.Input{companion}}}} result, err := configkit.LoadLayered(configkit.Input{Source: configkit.File(link), Filesystem: &configkit.FilesystemContext{Root: root}}, opts) if err != nil { t.Fatalf("%v: %#v", err, err.(*configkit.LoadError).Diagnostics()) } if got := result.Config.Settings["value"]; got != "derived" { t.Fatalf("derived value = %q", got) } result, err = configkit.LoadLayered(configkit.Input{Source: configkit.NoSource()}, opts) if err != nil { t.Fatalf("%v: %#v", err, err.(*configkit.LoadError).Diagnostics()) } if got := result.Config.Settings["value"]; got != "fallback" { t.Fatalf("fallback value = %q", got) } } func TestMainRelativeWithoutBase(t *testing.T) { tests := []struct { name, source string requireBase bool wantCode configkit.Code }{ {"optional", "config: {imports: [{path: missing.yml}]}\ncount: 7\n", false, ""}, {"required", "config: {imports: [{path: missing.yml, required: true}]}\n", false, configkit.CodeImportMissing}, {"required directory", "config: {imports: [{dir: missing, required: true}]}\n", false, configkit.CodeImportMissing}, {"required glob", "config: {imports: [{glob: '*.yml', required: true}]}\n", false, configkit.CodeImportMissing}, {"empty required", "config: {imports: [{path: [], required: true}]}\ncount: 7\n", false, ""}, {"base required", "count: 7\n", true, configkit.CodeOptionsInvalid}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { input := configkit.Input{Source: configkit.Bytes("companion", []byte(test.source)), Imports: true, MainRelative: &configkit.MainRelativeContext{RequireBase: test.requireBase}} result, err := configkit.LoadLayered(configkit.Input{Source: configkit.NoSource()}, configkit.LayeredOptions[layeredFacadeConfig]{Underlays: []configkit.Underlay{{Inputs: []configkit.Input{input}}}}) if test.wantCode != "" { if err == nil { t.Fatal("expected failure") } for _, d := range err.(*configkit.LoadError).Diagnostics() { if d.Code == test.wantCode { return } } t.Fatalf("diagnostics: %#v", err.(*configkit.LoadError).Diagnostics()) } if err != nil { t.Fatalf("%v: %#v", err, err.(*configkit.LoadError).Diagnostics()) } if result.Config.Count != 7 { t.Fatalf("config = %#v", result.Config) } if sources := result.Sources(); len(sources) != 1 || len(sources[0].EmptyImportSelections) != 0 { t.Fatalf("sources = %#v", sources) } }) } } func TestMainRelativeWithoutBaseSequenceItem(t *testing.T) { type item struct { Name string `yaml:"name"` } type config struct { Items []item `yaml:"items"` } input := configkit.Input{Source: configkit.Bytes("item", []byte("config: {imports: [{path: missing.yml}]}\nname: example\n")), Imports: true, MainRelative: configkit.MainRelative(), Destination: configkit.Keys("items"), PlacementMode: configkit.PlacementModeSequenceItem} result, err := configkit.LoadLayered(configkit.Input{Source: configkit.NoSource()}, configkit.LayeredOptions[config]{Underlays: []configkit.Underlay{{Inputs: []configkit.Input{input}}}}) if err != nil { t.Fatalf("%v: %#v", err, err.(*configkit.LoadError).Diagnostics()) } if len(result.Config.Items) != 1 || result.Config.Items[0].Name != "example" { t.Fatalf("config = %#v", result.Config) } } func TestMainRelativeWithoutBaseImportDefaults(t *testing.T) { required := true tests := []struct { name, source string imports bool requireBase bool code configkit.Code }{ {"default required", "config: {imports: [{path: missing.yml}]}\n", true, false, configkit.CodeImportMissing}, {"malformed", "config: {imports: [{path: 42}]}\n", true, false, configkit.CodeImportInvalid}, {"imports disabled", "config: {imports: [{path: missing.yml}]}\n", false, false, configkit.CodeImportDisabled}, {"base required without imports", "count: 1\n", false, true, configkit.CodeOptionsInvalid}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { input := configkit.Input{Source: configkit.Bytes("companion", []byte(test.source)), Imports: test.imports, MainRelative: &configkit.MainRelativeContext{RequireBase: test.requireBase}} _, err := configkit.LoadLayered(configkit.Input{Source: configkit.NoSource()}, configkit.LayeredOptions[layeredFacadeConfig]{ImportDefaults: configkit.ImportDefaults{Required: &required}, Underlays: []configkit.Underlay{{Inputs: []configkit.Input{input}}}}) if err == nil { t.Fatal("expected failure") } for _, diagnostic := range err.(*configkit.LoadError).Diagnostics() { if diagnostic.Code == test.code { return } } t.Fatalf("diagnostics = %#v", err.(*configkit.LoadError).Diagnostics()) }) } } func TestLayeredOverlayContextSelection(t *testing.T) { root := t.TempDir() base := filepath.Join(root, "base") for _, dir := range []string{filepath.Join(root, "overlays"), filepath.Join(base, "overlays")} { if err := os.MkdirAll(dir, 0700); err != nil { t.Fatal(err) } } for path, content := range map[string]string{"base/main.yml": "count: 1\n", "base/overlays/test.yml": "count: 2\n", "overlays/test.yml": "count: 3\n"} { if err := os.WriteFile(filepath.Join(root, path), []byte(content), 0600); err != nil { t.Fatal(err) } } main := configkit.Input{Source: configkit.File(filepath.Join(base, "main.yml")), Filesystem: &configkit.FilesystemContext{Root: root}} result, err := configkit.LoadLayered(main, configkit.LayeredOptions[layeredFacadeConfig]{Overlay: "test"}) if err != nil { t.Fatalf("%v: %#v", err, err.(*configkit.LoadError).Diagnostics()) } if result.Config.Count != 2 { t.Fatalf("derived overlay = %d", result.Config.Count) } result, err = configkit.LoadLayered(main, configkit.LayeredOptions[layeredFacadeConfig]{Overlay: "test", OverlayFilesystem: &configkit.FilesystemContext{Root: root, BaseDir: base}}) if err != nil { t.Fatalf("%v: %#v", err, err.(*configkit.LoadError).Diagnostics()) } if result.Config.Count != 3 { t.Fatalf("explicit overlay = %d", result.Config.Count) } result, err = configkit.LoadLayered(configkit.Input{Source: configkit.NoSource()}, configkit.LayeredOptions[layeredFacadeConfig]{Overlay: "test", OverlayFallback: &configkit.FilesystemContext{Root: root, BaseDir: base}}) if err != nil { t.Fatalf("%v: %#v", err, err.(*configkit.LoadError).Diagnostics()) } if result.Config.Count != 2 { t.Fatalf("fallback overlay = %d", result.Config.Count) } } func TestLayeredOverlayNativeContext(t *testing.T) { filesystem := configkit.NewFilesystem("bundle", fstest.MapFS{ "dir/main.yml": &fstest.MapFile{Data: []byte("count: 1\n")}, "dir/overlays/test.yml": &fstest.MapFile{Data: []byte("count: 9\n")}, }) main := configkit.Input{Source: filesystem.File("dir/main.yml"), Filesystem: &configkit.FilesystemContext{Root: ".", FS: &filesystem}} result, err := configkit.LoadLayered(main, configkit.LayeredOptions[layeredFacadeConfig]{Overlay: "test"}) if err != nil { t.Fatalf("%v: %#v", err, err.(*configkit.LoadError).Diagnostics()) } if result.Config.Count != 9 { t.Fatalf("config = %#v", result.Config) } _, err = configkit.LoadLayered(main, configkit.LayeredOptions[layeredFacadeConfig]{Overlay: "missing", OverlayFallback: &configkit.FilesystemContext{Root: ".", BaseDir: ".", FS: &filesystem}}) if err == nil { t.Fatal("missing selected overlay succeeded") } for _, diagnostic := range err.(*configkit.LoadError).Diagnostics() { if diagnostic.Code == configkit.CodeOverlayMissing { return } } t.Fatalf("diagnostics = %#v", err.(*configkit.LoadError).Diagnostics()) } func TestLayeredOverlayNativeLinks(t *testing.T) { for _, tc := range []struct { name, mainPath, target string want configkit.Code }{ {"outside root", "scope/main.yml", "../../outside.yml", configkit.CodeOverlayOutsideRoot}, {"overlay aliases main", "scope/main.yml", "../main.yml", configkit.CodeOverlayRepeated}, {"main aliases overlay target", "scope/alias.yml", "../target.yml", configkit.CodeOverlayRepeated}, {"contained overlay", "scope/main.yml", "../target.yml", ""}, } { t.Run(tc.name, func(t *testing.T) { native := configkit.NewFilesystem("bundle", fstest.MapFS{ "scope/main.yml": {Data: []byte("count: 1\n")}, "scope/alias.yml": {Data: []byte("target.yml"), Mode: os.ModeSymlink}, "scope/target.yml": {Data: []byte("count: 7\n")}, "scope/overlays/test.yml": {Data: []byte(tc.target), Mode: os.ModeSymlink}, "outside.yml": {Data: []byte("count: 9\n")}, }) main := configkit.Input{Source: native.File(tc.mainPath), Filesystem: &configkit.FilesystemContext{Root: "scope", FS: &native}} result, err := configkit.LoadLayered(main, configkit.LayeredOptions[layeredFacadeConfig]{Overlay: "test"}) if tc.want != "" { if err == nil { t.Fatal("overlay link unexpectedly loaded") } diagnostics := err.(*configkit.LoadError).Diagnostics() if len(diagnostics) != 1 || diagnostics[0].Code != tc.want { t.Fatalf("diagnostics = %#v, want %v", diagnostics, tc.want) } return } if err != nil { t.Fatal(err) } if result.Config.Count != 7 || result.Sources()[1].Path != "scope/target.yml" { t.Fatalf("config = %#v, sources = %#v", result.Config, result.Sources()) } }) } }