244 lines
12 KiB
Go
244 lines
12 KiB
Go
package configkit_test
|
|
|
|
import (
|
|
"bytes"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"testing/fstest"
|
|
|
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
|
)
|
|
|
|
type layeredFacadeConfig struct {
|
|
Settings map[string]string `yaml:"settings"`
|
|
Count int `yaml:"count"`
|
|
Enabled bool `yaml:"enabled"`
|
|
List []string `yaml:"list"`
|
|
Optional *string `yaml:"optional"`
|
|
Derived string `yaml:"derived"`
|
|
}
|
|
|
|
func TestFacadeLayeredMixedSourcesAndInspection(t *testing.T) {
|
|
mainPath := filepath.Join(t.TempDir(), "main.yml")
|
|
if err := os.WriteFile(mainPath, []byte("settings: {shared: main, secret: new-canary}\ncount: 0\nenabled: false\nlist: []\noptional: null\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
lower := configkit.NewFilesystem("bundle", fstest.MapFS{
|
|
"main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: defaults.yml}]}\nsettings: {shared: lower, native: retained, secret: old-canary}\nlist: [lower]\n")},
|
|
"defaults.yml": &fstest.MapFile{Data: []byte("settings: {imported: retained}\n")},
|
|
"overlays/testing.yml": &fstest.MapFile{Data: []byte("settings: {from_lower_namespace: retained}\n")},
|
|
})
|
|
selected := configkit.NewFilesystem("bundle", fstest.MapFS{
|
|
"overlays/testing.yml": &fstest.MapFile{Data: []byte("settings: {shared: overlay}\n")},
|
|
})
|
|
checks := struct{ defaults, resolution, validation int }{}
|
|
result, err := configkit.LoadLayered(configkit.Input{Source: configkit.File(mainPath)}, configkit.LayeredOptions[layeredFacadeConfig]{
|
|
Underlays: []configkit.Underlay{
|
|
{Inputs: []configkit.Input{
|
|
{Source: lower.File("main.yml"), Imports: true},
|
|
{Source: configkit.Mapping("programmatic-low", map[string]any{"settings": map[string]any{"mapping": "retained"}, "count": 7, "enabled": true})},
|
|
{Source: lower.File("overlays/testing.yml")},
|
|
}},
|
|
{Inputs: []configkit.Input{{Source: configkit.Mapping("programmatic-high", map[string]any{
|
|
"settings": map[string]any{"shared": "middle", "second": "retained"}, "list": []any{"middle"},
|
|
})}}},
|
|
},
|
|
Overlay: "testing", OverlayFilesystem: &configkit.FilesystemContext{Root: ".", BaseDir: ".", FS: &selected},
|
|
Overrides: []configkit.Override{{Path: configkit.Keys("settings", "shared"), Value: "cli", Flag: "--shared"}},
|
|
Definition: configkit.Definition[layeredFacadeConfig]{
|
|
Defaults: func() layeredFacadeConfig { checks.defaults++; return layeredFacadeConfig{} },
|
|
Resolve: func(config *layeredFacadeConfig, resolution *configkit.Resolution) error {
|
|
checks.resolution++
|
|
if source, ok := resolution.Source("source-2"); !ok || source.Layer != configkit.LayerUnderlay || source.UnderlayIndex != 1 || source.Kind != configkit.SourceKindFilesystem {
|
|
t.Errorf("resolver source = %#v, %v", source, ok)
|
|
}
|
|
configkit.Default(resolution, configkit.Keys("derived"), &config.Derived, config.Settings["native"], "derive from native", configkit.Keys("settings", "native"))
|
|
return nil
|
|
},
|
|
Validate: func(config *layeredFacadeConfig, validation *configkit.Validation) { checks.validation++ },
|
|
Secret: func(path configkit.Path) bool { return path.String() == "/settings/secret" },
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("layered load: %v, diagnostics %#v", err, err.(*configkit.LoadError).Diagnostics())
|
|
}
|
|
wantSettings := map[string]string{
|
|
"shared": "cli", "secret": "new-canary", "native": "retained", "imported": "retained",
|
|
"mapping": "retained", "from_lower_namespace": "retained", "second": "retained",
|
|
}
|
|
if !reflect.DeepEqual(result.Config.Settings, wantSettings) || result.Config.Count != 0 || result.Config.Enabled || result.Config.List == nil || len(result.Config.List) != 0 || result.Config.Optional != nil || result.Config.Derived != "retained" {
|
|
t.Fatalf("configuration = %#v", result.Config)
|
|
}
|
|
if checks != (struct{ defaults, resolution, validation int }{1, 1, 1}) {
|
|
t.Fatalf("lifecycle calls = %#v", checks)
|
|
}
|
|
sources := result.Sources()
|
|
if len(sources) != 8 {
|
|
t.Fatalf("sources = %#v", sources)
|
|
}
|
|
wantLayers := []configkit.Layer{configkit.LayerBase, configkit.LayerUnderlay, configkit.LayerUnderlay, configkit.LayerUnderlay, configkit.LayerUnderlay, configkit.LayerUnderlay, configkit.LayerOverlay, configkit.LayerCLI}
|
|
wantIndexes := []int{0, 1, 1, 1, 1, 2, 0, 0}
|
|
wantKinds := []configkit.SourceKind{configkit.SourceKindFile, configkit.SourceKindFilesystem, configkit.SourceKindFilesystem, configkit.SourceKindMapping, configkit.SourceKindFilesystem, configkit.SourceKindMapping, configkit.SourceKindFilesystem, configkit.SourceKindCLI}
|
|
for index, source := range sources {
|
|
if source.ID != "source-"+strconv.Itoa(index+1) || source.Layer != wantLayers[index] || source.UnderlayIndex != wantIndexes[index] || source.Kind != wantKinds[index] {
|
|
t.Errorf("source %d = %#v", index, source)
|
|
}
|
|
}
|
|
if sources[4].Path != sources[6].Path || sources[4].Label != sources[6].Label {
|
|
t.Fatalf("namespace-collision fixture changed: %#v, %#v", sources[4], sources[6])
|
|
}
|
|
sources[1].Label = "changed"
|
|
if result.Sources()[1].Label != "bundle/main.yml" {
|
|
t.Fatal("source records alias retained metadata")
|
|
}
|
|
for _, test := range []struct {
|
|
path string
|
|
want configkit.Layer
|
|
}{
|
|
{"/settings/native", configkit.LayerUnderlay},
|
|
{"/settings/shared", configkit.LayerCLI},
|
|
{"/derived", configkit.LayerResolution},
|
|
} {
|
|
path, err := configkit.ParsePath(test.path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
info, ok := result.Metadata().Lookup(path)
|
|
if !ok || info.Origin.Layer != test.want {
|
|
t.Errorf("%s metadata = %#v, %v", test.path, info, ok)
|
|
}
|
|
}
|
|
container, ok := result.Metadata().Lookup(configkit.Keys("settings"))
|
|
if !ok || len(container.Origin.Locations) < 4 {
|
|
t.Fatalf("composite mapping origin = %#v, %v", container, ok)
|
|
}
|
|
explanation, ok := result.Explain(configkit.Keys("settings", "shared"))
|
|
if !ok || len(explanation.History) < 5 || explanation.Origin.Layer != configkit.LayerCLI {
|
|
t.Fatalf("shared history = %#v, %v", explanation, ok)
|
|
}
|
|
var effective, rendered bytes.Buffer
|
|
if err := result.WriteEffective(&effective); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := result.WriteExplanation(&rendered, configkit.Keys("settings", "secret")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(effective.String()+rendered.String(), "old-canary") || strings.Contains(effective.String()+rendered.String(), "new-canary") || !strings.Contains(rendered.String(), "redacted") {
|
|
t.Fatalf("secret rendering leaked: effective %q, explanation %q", effective.String(), rendered.String())
|
|
}
|
|
}
|
|
|
|
func TestFacadeLayeredDisplacedDescendantRemainsInspectable(t *testing.T) {
|
|
type config struct {
|
|
Choice string `yaml:"choice"`
|
|
}
|
|
result, err := configkit.LoadLayered(configkit.Input{Source: configkit.Mapping("main", map[string]any{"choice": "flat"})}, configkit.LayeredOptions[config]{
|
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Mapping("lower", map[string]any{
|
|
"choice": map[string]any{"nested": "historical"},
|
|
})}}}},
|
|
})
|
|
if err != nil || result.Config.Choice != "flat" {
|
|
t.Fatalf("replacement load = %#v, %v", result, err)
|
|
}
|
|
path := configkit.Keys("choice", "nested")
|
|
explanation, ok := result.Explain(path)
|
|
if !ok || explanation.Effective.Present || len(explanation.History) == 0 || explanation.History[0].Origin.Layer != configkit.LayerUnderlay {
|
|
t.Fatalf("deleted descendant = %#v, %v", explanation, ok)
|
|
}
|
|
}
|
|
|
|
func TestFacadeLayeredZeroUnderlaysMatchesOrdinaryLoad(t *testing.T) {
|
|
filesystem := configkit.NewFilesystem("bundle", fstest.MapFS{
|
|
"overlays/testing.yml": &fstest.MapFile{Data: []byte("value: selected\n")},
|
|
})
|
|
context := &configkit.FilesystemContext{Root: ".", BaseDir: ".", FS: &filesystem}
|
|
source := configkit.Bytes("main", []byte("value: base\n"))
|
|
type config struct {
|
|
Value string `yaml:"value"`
|
|
}
|
|
ordinary, err := configkit.Load(source, configkit.Options[config]{Overlay: "testing", Filesystem: context})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
layered, err := configkit.LoadLayered(configkit.Input{Source: source}, configkit.LayeredOptions[config]{Overlay: "testing", OverlayFilesystem: context})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if ordinary.Config != layered.Config || !reflect.DeepEqual(ordinary.Sources(), layered.Sources()) {
|
|
t.Fatalf("ordinary = %#v, %#v; layered = %#v, %#v", ordinary.Config, ordinary.Sources(), layered.Config, layered.Sources())
|
|
}
|
|
var first, second bytes.Buffer
|
|
if err := ordinary.WriteEffective(&first); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := layered.WriteEffective(&second); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if first.String() != second.String() {
|
|
t.Fatalf("effective ordinary %q, layered %q", first.String(), second.String())
|
|
}
|
|
if ordinaryExplanation, ok := ordinary.Explain(configkit.Keys("value")); !ok {
|
|
t.Fatal("ordinary explanation missing")
|
|
} else if layeredExplanation, ok := layered.Explain(configkit.Keys("value")); !ok || !reflect.DeepEqual(ordinaryExplanation, layeredExplanation) {
|
|
t.Fatalf("explanations differ: %#v, %#v", ordinaryExplanation, layeredExplanation)
|
|
}
|
|
}
|
|
|
|
func TestFacadeLayeredOptionAndVersionFailures(t *testing.T) {
|
|
type config struct {
|
|
Version string `yaml:"version"`
|
|
}
|
|
namespace := configkit.NewFilesystem("bundle", fstest.MapFS{
|
|
"overlays/testing.yml": &fstest.MapFile{Data: []byte("{}\n")},
|
|
})
|
|
for _, test := range []struct {
|
|
name string
|
|
main configkit.Input
|
|
options []configkit.LayeredOptions[config]
|
|
want configkit.Code
|
|
}{
|
|
{"zero source", configkit.Input{}, nil, configkit.CodeSourceInvalid},
|
|
{"multiple options", configkit.Input{Source: configkit.NoSource()}, []configkit.LayeredOptions[config]{{}, {}}, configkit.CodeOptionsInvalid},
|
|
{"missing overlay context", configkit.Input{Source: configkit.NoSource()}, []configkit.LayeredOptions[config]{{Overlay: "testing"}}, configkit.CodeOptionsInvalid},
|
|
{"selected file already acquired", configkit.Input{Source: configkit.NoSource()}, []configkit.LayeredOptions[config]{{
|
|
Overlay: "testing", OverlayFilesystem: &configkit.FilesystemContext{Root: ".", BaseDir: ".", FS: &namespace},
|
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: namespace.File("overlays/testing.yml")}}}},
|
|
}}, configkit.CodeOverlayRepeated},
|
|
{"underlay version", configkit.Input{Source: configkit.NoSource()}, []configkit.LayeredOptions[config]{{
|
|
Definition: configkit.Definition[config]{Version: &configkit.VersionRule{Path: configkit.Keys("version"), Check: func(configkit.Version, *configkit.Validation) error { return nil }}},
|
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Mapping("lower", map[string]any{"version": "forbidden"})}}}},
|
|
}}, configkit.CodeUnderlayVersion},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
_, err := configkit.LoadLayered(test.main, test.options...)
|
|
loadError, ok := err.(*configkit.LoadError)
|
|
if !ok || len(loadError.Diagnostics()) == 0 || loadError.Diagnostics()[0].Code != test.want {
|
|
t.Fatalf("error = %#v, want %s", err, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFacadeLayeredExplicitHostOverlayWithoutMain(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := os.Mkdir(filepath.Join(root, "overlays"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(root, "overlays", "testing.yml"), []byte("value: selected\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
type config struct {
|
|
Value string `yaml:"value"`
|
|
}
|
|
result, err := configkit.LoadLayered(configkit.Input{Source: configkit.NoSource()}, configkit.LayeredOptions[config]{
|
|
Overlay: "testing", OverlayFilesystem: &configkit.FilesystemContext{Root: root, BaseDir: root},
|
|
})
|
|
if err != nil || result.Config.Value != "selected" || len(result.Sources()) != 1 || result.Sources()[0].Kind != configkit.SourceKindFile {
|
|
t.Fatalf("explicit host overlay = %#v, %v", result, err)
|
|
}
|
|
}
|