165 lines
7.0 KiB
Go
165 lines
7.0 KiB
Go
package configkit_test
|
|
|
|
import (
|
|
"bytes"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"testing/fstest"
|
|
|
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
|
)
|
|
|
|
type equivalentConfig struct {
|
|
Version uint64 `yaml:"version"`
|
|
Name string `yaml:"name"`
|
|
Enabled bool `yaml:"enabled"`
|
|
Count int `yaml:"count"`
|
|
Items []string `yaml:"items"`
|
|
Optional *string `yaml:"optional"`
|
|
}
|
|
|
|
func TestEquivalentSourceFormsPreserveInputSemantics(t *testing.T) {
|
|
const yaml = "version: 1\nname: retained\nenabled: false\ncount: 0\nitems: []\noptional: null\n"
|
|
hostPath := filepath.Join(t.TempDir(), "main.yml")
|
|
if err := os.WriteFile(hostPath, []byte(yaml), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
namespace := configkit.NewFilesystem("bundle", fstest.MapFS{"main.yml": &fstest.MapFile{Data: []byte(yaml)}})
|
|
context := &configkit.FilesystemContext{Root: ".", BaseDir: ".", FS: &namespace}
|
|
for _, test := range []struct {
|
|
name string
|
|
source configkit.Source
|
|
context *configkit.FilesystemContext
|
|
kind configkit.SourceKind
|
|
}{
|
|
{"host", configkit.File(hostPath), nil, configkit.SourceKindFile},
|
|
{"native", namespace.File("main.yml"), nil, configkit.SourceKindFilesystem},
|
|
{"bytes", configkit.Bytes("memory", []byte(yaml)), context, configkit.SourceKindBytes},
|
|
{"reader", configkit.Reader("stream", strings.NewReader(yaml)), context, configkit.SourceKindReader},
|
|
{"mapping", configkit.Mapping("programmatic", map[string]any{
|
|
"version": uint64(1), "name": "retained", "enabled": false, "count": 0,
|
|
"items": []any{}, "optional": nil,
|
|
}), context, configkit.SourceKindMapping},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
checks := 0
|
|
result, err := configkit.LoadLayered(configkit.Input{Source: test.source, Filesystem: test.context}, configkit.LayeredOptions[equivalentConfig]{
|
|
Definition: configkit.Definition[equivalentConfig]{Version: &configkit.VersionRule{
|
|
Path: configkit.Keys("version"), Check: func(version configkit.Version, validation *configkit.Validation) error {
|
|
checks++
|
|
if !version.Present || version.Text != "1" {
|
|
t.Errorf("version = %#v", version)
|
|
}
|
|
return nil
|
|
},
|
|
}},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := equivalentConfig{Version: 1, Name: "retained", Items: []string{}}
|
|
if !reflect.DeepEqual(result.Config, want) || checks != 1 {
|
|
t.Fatalf("config = %#v, checks %d", result.Config, checks)
|
|
}
|
|
for _, field := range []string{"version", "name", "enabled", "count", "items", "optional"} {
|
|
info, ok := result.Metadata().Lookup(configkit.Keys(field))
|
|
if !ok || !info.Explicit || info.Origin.Layer != configkit.LayerBase || info.Null != (field == "optional") {
|
|
t.Errorf("%s input info = %#v, %v", field, info, ok)
|
|
}
|
|
}
|
|
sources := result.Sources()
|
|
if len(sources) != 1 || sources[0].Kind != test.kind || sources[0].ID != "source-1" || sources[0].UnderlayIndex != 0 {
|
|
t.Fatalf("sources = %#v", sources)
|
|
}
|
|
resource, ok := result.SourceContext("source-1")
|
|
if !ok || resource.BaseDir != "." || resource.FS == nil {
|
|
t.Fatalf("source context = %#v, %v", resource, ok)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRepeatedImportsAcrossTiersAndReaderConsumption(t *testing.T) {
|
|
namespace := configkit.NewFilesystem("bundle", fstest.MapFS{
|
|
"left.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: shared.yml}]}\n")},
|
|
"right.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: shared.yml}]}\n")},
|
|
"shared.yml": &fstest.MapFile{Data: []byte("name: retained\n")},
|
|
})
|
|
type config struct {
|
|
Name string `yaml:"name"`
|
|
}
|
|
left := configkit.Input{Source: namespace.File("left.yml"), Imports: true}
|
|
right := configkit.Input{Source: namespace.File("right.yml"), Imports: true}
|
|
_, err := configkit.LoadLayered(configkit.Input{Source: configkit.NoSource()}, configkit.LayeredOptions[config]{
|
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{left, right}}},
|
|
})
|
|
if got := layeredErrorCode(t, err); got != configkit.CodeImportRepeated {
|
|
t.Fatalf("same-tier repeated import = %s", got)
|
|
}
|
|
result, err := configkit.LoadLayered(configkit.Input{Source: configkit.NoSource()}, configkit.LayeredOptions[config]{
|
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{left}}, {Inputs: []configkit.Input{right}}},
|
|
})
|
|
if err != nil || result.Config.Name != "retained" || len(result.Sources()) != 4 {
|
|
t.Fatalf("cross-tier repeated import = %#v, %v", result, err)
|
|
}
|
|
reader := configkit.Reader("single-use", strings.NewReader("name: retained\n"))
|
|
_, err = configkit.LoadLayered(configkit.Input{Source: configkit.NoSource()}, configkit.LayeredOptions[config]{
|
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: reader}}}, {Inputs: []configkit.Input{{Source: reader}}}},
|
|
})
|
|
if got := layeredErrorCode(t, err); got != configkit.CodeSourceReaderUsed {
|
|
t.Fatalf("reader re-consumption = %s", got)
|
|
}
|
|
}
|
|
|
|
func TestReplacedSecretRemainsAbsentFromSafeOutput(t *testing.T) {
|
|
type config struct {
|
|
Secret string `yaml:"secret"`
|
|
}
|
|
result, err := configkit.LoadLayered(configkit.Input{Source: configkit.Bytes("main", []byte("secret: new-secret-canary\n"))}, configkit.LayeredOptions[config]{
|
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Mapping("lower", map[string]any{"secret": "old-secret-canary"})}}}},
|
|
Definition: configkit.Definition[config]{Secret: func(path configkit.Path) bool { return path.String() == "/secret" }},
|
|
})
|
|
if err != nil || result.Config.Secret != "new-secret-canary" {
|
|
t.Fatalf("secret load = %#v, %v", result, err)
|
|
}
|
|
var effective, history bytes.Buffer
|
|
if err := result.WriteEffective(&effective); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := result.WriteExplanation(&history, configkit.Keys("secret")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(effective.String()+history.String(), "old-secret-canary") || strings.Contains(effective.String()+history.String(), "new-secret-canary") {
|
|
t.Fatalf("secret output leaked: %q %q", effective.String(), history.String())
|
|
}
|
|
_, err = configkit.LoadLayered(configkit.Input{Source: configkit.Bytes("main", []byte("secret: new-secret-canary\n"))}, configkit.LayeredOptions[config]{
|
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Mapping("lower", map[string]any{"secret": "old-secret-canary"})}}}},
|
|
Definition: configkit.Definition[config]{Validate: func(config *config, validation *configkit.Validation) {
|
|
validation.Error(configkit.Keys("secret"), "invalid secret")
|
|
}},
|
|
})
|
|
loadError, ok := err.(*configkit.LoadError)
|
|
if !ok {
|
|
t.Fatalf("validation error = %#v", err)
|
|
}
|
|
var diagnostics bytes.Buffer
|
|
if err := configkit.WriteDiagnostics(&diagnostics, loadError.Diagnostics()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(diagnostics.String(), "secret-canary") {
|
|
t.Fatalf("diagnostic leaked secret: %q", diagnostics.String())
|
|
}
|
|
}
|
|
|
|
func layeredErrorCode(t *testing.T, err error) configkit.Code {
|
|
t.Helper()
|
|
loadError, ok := err.(*configkit.LoadError)
|
|
if !ok || len(loadError.Diagnostics()) == 0 {
|
|
t.Fatalf("load error = %#v", err)
|
|
}
|
|
return loadError.Diagnostics()[0].Code
|
|
}
|