Files
ConfigKit/facade_layered_test.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)
}
}