706 lines
30 KiB
Go
706 lines
30 KiB
Go
package configkit_test
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"testing/fstest"
|
|
|
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
|
)
|
|
|
|
func TestFacadeInputDestinationsAcrossSourceKinds(t *testing.T) {
|
|
directory := t.TempDir()
|
|
hostPath := filepath.Join(directory, "host.yml")
|
|
if err := os.WriteFile(hostPath, []byte("value: host\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
native := configkit.NewFilesystem("native", fstest.MapFS{
|
|
"main.yml": &fstest.MapFile{Data: []byte("value: native\n")},
|
|
})
|
|
const literalValuePath = "/collections/weather.reporter/profiles/a~1b/~0/0"
|
|
destination := configkit.Keys("collections", "weather.reporter", "profiles", "a/b", "~", "0")
|
|
for _, test := range []struct {
|
|
name string
|
|
source configkit.Source
|
|
want string
|
|
kind configkit.SourceKind
|
|
}{
|
|
{name: "host file", source: configkit.File(hostPath), want: "host", kind: configkit.SourceKindFile},
|
|
{name: "native filesystem", source: native.File("main.yml"), want: "native", kind: configkit.SourceKindFilesystem},
|
|
{name: "bytes", source: configkit.Bytes("bytes", []byte("value: bytes\n")), want: "bytes", kind: configkit.SourceKindBytes},
|
|
{name: "reader", source: configkit.Reader("reader", strings.NewReader("value: reader\n")), want: "reader", kind: configkit.SourceKindReader},
|
|
{name: "mapping", source: configkit.Mapping("mapping", map[string]any{"value": "mapping"}), want: "mapping", kind: configkit.SourceKindMapping},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
result, err := configkit.LoadLayered[map[string]any](configkit.Input{Source: test.source, Destination: destination})
|
|
if err != nil {
|
|
t.Fatalf("LoadLayered: %v", err)
|
|
}
|
|
if got := facadeDestinationValue(t, result.Config, "collections", "weather.reporter", "profiles", "a/b", "~", "0", "value"); got != test.want {
|
|
t.Fatalf("placed value = %#v, want %q", got, test.want)
|
|
}
|
|
sources := result.Sources()
|
|
if len(sources) != 1 || sources[0].Kind != test.kind || sources[0].Destination.String() != destination.String() || destination.String() != literalValuePath {
|
|
t.Fatalf("source placement = %#v, destination = %q", sources, destination.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFacadeDestinationRootEmptyMappingNoSourceAndInvalidPaths(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
destination configkit.Path
|
|
}{
|
|
{name: "omitted destination"},
|
|
{name: "explicit empty destination", destination: configkit.Path{}},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
result, err := configkit.LoadLayered[map[string]any](configkit.Input{Source: configkit.Bytes("root", []byte("value: root\n")), Destination: test.destination})
|
|
if err != nil {
|
|
t.Fatalf("root load: %v", err)
|
|
}
|
|
if result.Config["value"] != "root" || len(result.Sources()) != 1 || result.Sources()[0].Destination.String() != "" {
|
|
t.Fatalf("root placement = %#v, sources %#v", result.Config, result.Sources())
|
|
}
|
|
})
|
|
}
|
|
|
|
emptyPath := configkit.Keys("collections", "empty")
|
|
empty, err := configkit.LoadLayered[map[string]any](configkit.Input{
|
|
Source: configkit.Mapping("empty", map[string]any{}), Destination: emptyPath,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("empty mapping load: %v", err)
|
|
}
|
|
if value := facadeDestinationMap(t, empty.Config, "collections")["empty"]; value == nil {
|
|
t.Fatal("empty mapping did not materialize its destination")
|
|
}
|
|
if info, ok := empty.Metadata().Lookup(emptyPath); !ok || !info.Explicit {
|
|
t.Fatalf("empty destination metadata = %#v, %v", info, ok)
|
|
}
|
|
|
|
absentPath := configkit.Keys("collections", "absent")
|
|
absent, err := configkit.LoadLayered[map[string]any](configkit.Input{Source: configkit.NoSource(), Destination: absentPath})
|
|
if err != nil {
|
|
t.Fatalf("NoSource with a destination: %v", err)
|
|
}
|
|
if len(absent.Sources()) != 0 {
|
|
t.Fatalf("NoSource created source records: %#v", absent.Sources())
|
|
}
|
|
if _, ok := absent.Metadata().Lookup(absentPath); ok {
|
|
t.Fatal("NoSource materialized its destination")
|
|
}
|
|
|
|
reader := &destinationCountingReader{}
|
|
for _, test := range []struct {
|
|
name string
|
|
source configkit.Source
|
|
destination configkit.Path
|
|
wantReads int
|
|
}{
|
|
{name: "index segment", source: configkit.Reader("unread", reader), destination: configkit.Keys("catalog").Index(0)},
|
|
{name: "reserved configkit key", source: configkit.NoSource(), destination: configkit.Keys("configkit")},
|
|
{name: "reserved extends key", source: configkit.NoSource(), destination: configkit.Keys("catalog", "extends")},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
_, err := configkit.LoadLayered[map[string]any](configkit.Input{Source: test.source, Destination: test.destination})
|
|
loadError, ok := err.(*configkit.LoadError)
|
|
if !ok || len(loadError.Diagnostics()) == 0 || loadError.Diagnostics()[0].Code != configkit.CodeOptionsInvalid {
|
|
t.Fatalf("invalid destination error = %#v", err)
|
|
}
|
|
if reader.reads != test.wantReads {
|
|
t.Fatalf("reader calls = %d, want %d", reader.reads, test.wantReads)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFacadeImportedSourcesRetainInputDestination(t *testing.T) {
|
|
directory := t.TempDir()
|
|
mainPath := filepath.Join(directory, "main.yml")
|
|
fragmentPath := filepath.Join(directory, "fragment.yml")
|
|
if err := os.WriteFile(mainPath, []byte("configkit: {imports: [{path: fragment.yml}]}\nmain: present\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(fragmentPath, []byte("imported: present\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
destination := configkit.Keys("collections", "weatherreporter")
|
|
result, err := configkit.LoadLayered[map[string]any](configkit.Input{
|
|
Source: configkit.File(mainPath), Imports: true, Destination: destination,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("mounted import load: %v", err)
|
|
}
|
|
collection := facadeDestinationMap(t, result.Config, "collections", "weatherreporter")
|
|
if collection["main"] != "present" || collection["imported"] != "present" {
|
|
t.Fatalf("mounted imported values = %#v", collection)
|
|
}
|
|
sources := result.Sources()
|
|
if len(sources) != 2 || sources[0].Destination.String() != destination.String() || sources[1].Destination.String() != destination.String() {
|
|
t.Fatalf("mounted import source destinations = %#v", sources)
|
|
}
|
|
if sources[1].ImportedFrom == nil {
|
|
t.Fatalf("imported source lacks its authored directive: %#v", sources[1])
|
|
}
|
|
}
|
|
|
|
func TestFacadeRelativeImportDestinationsForRootAndMountedLoads(t *testing.T) {
|
|
directory := t.TempDir()
|
|
write := func(relative, contents string) string {
|
|
t.Helper()
|
|
filename := filepath.Join(directory, filepath.FromSlash(relative))
|
|
if err := os.MkdirAll(filepath.Dir(filename), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filename, []byte(contents), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return filename
|
|
}
|
|
|
|
mainPath := write("main.yml", `configkit:
|
|
imports:
|
|
- path: metadata.yml
|
|
destination: [metadata]
|
|
- glob: prompts/*.yml
|
|
destination: [prompts]
|
|
- path: entries/main.yml
|
|
destination: [profiles]
|
|
- path: sibling.yml
|
|
destination: [sibling]
|
|
root: main
|
|
`)
|
|
write("metadata.yml", "name: imported\n")
|
|
write("prompts/a.yml", "a: {name: alpha}\n")
|
|
write("prompts/b.yml", "b: {name: beta}\n")
|
|
write("entries/main.yml", `configkit:
|
|
imports:
|
|
- path: nested/details.yml
|
|
destination: [details]
|
|
name: profile
|
|
`)
|
|
write("entries/nested/details.yml", "summary: nested\n")
|
|
write("sibling.yml", "name: separate\n")
|
|
|
|
root, err := configkit.Load(
|
|
configkit.File(mainPath), configkit.Options[map[string]any]{Imports: true},
|
|
)
|
|
if err != nil {
|
|
if loadError, ok := err.(*configkit.LoadError); ok {
|
|
t.Fatalf("ordinary root Load: %v, diagnostics %#v", err, loadError.Diagnostics())
|
|
}
|
|
t.Fatalf("ordinary root Load: %v", err)
|
|
}
|
|
mountedDestination := configkit.Keys("collections", "weatherreporter")
|
|
mounted, err := configkit.LoadLayered[map[string]any](configkit.Input{
|
|
Source: configkit.File(mainPath), Imports: true, Destination: mountedDestination,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("mounted layered Load: %v", err)
|
|
}
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
result *configkit.Result[map[string]any]
|
|
prefix []string
|
|
}{
|
|
{name: "ordinary root", result: root},
|
|
{name: "mounted entry", result: mounted, prefix: []string{"collections", "weatherreporter"}},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
at := func(keys ...string) []string {
|
|
return append(append([]string(nil), test.prefix...), keys...)
|
|
}
|
|
if got := facadeDestinationValue(t, test.result.Config, at("root")...); got != "main" {
|
|
t.Fatalf("root value = %#v", got)
|
|
}
|
|
if got := facadeDestinationValue(t, test.result.Config, at("metadata", "name")...); got != "imported" {
|
|
t.Fatalf("metadata value = %#v", got)
|
|
}
|
|
if got := facadeDestinationValue(t, test.result.Config, at("prompts", "a", "name")...); got != "alpha" {
|
|
t.Fatalf("first glob value = %#v", got)
|
|
}
|
|
if got := facadeDestinationValue(t, test.result.Config, at("prompts", "b", "name")...); got != "beta" {
|
|
t.Fatalf("second glob value = %#v", got)
|
|
}
|
|
if got := facadeDestinationValue(t, test.result.Config, at("profiles", "name")...); got != "profile" {
|
|
t.Fatalf("entry value = %#v", got)
|
|
}
|
|
if got := facadeDestinationValue(t, test.result.Config, at("profiles", "details", "summary")...); got != "nested" {
|
|
t.Fatalf("nested import value = %#v", got)
|
|
}
|
|
if got := facadeDestinationValue(t, test.result.Config, at("sibling", "name")...); got != "separate" {
|
|
t.Fatalf("subsequent sibling value = %#v", got)
|
|
}
|
|
|
|
wantPaths := []string{
|
|
"main.yml", "metadata.yml", "a.yml", "b.yml", "main.yml", "details.yml", "sibling.yml",
|
|
}
|
|
wantPhysicalPaths := []string{
|
|
mainPath,
|
|
filepath.Join(directory, "metadata.yml"),
|
|
filepath.Join(directory, "prompts", "a.yml"),
|
|
filepath.Join(directory, "prompts", "b.yml"),
|
|
filepath.Join(directory, "entries", "main.yml"),
|
|
filepath.Join(directory, "entries", "nested", "details.yml"),
|
|
filepath.Join(directory, "sibling.yml"),
|
|
}
|
|
wantImportParents := []string{"", "source-1", "source-1", "source-1", "source-1", "source-5", "source-1"}
|
|
wantDestinations := []configkit.Path{configkit.Keys(test.prefix...)}
|
|
for _, suffix := range [][]string{{"metadata"}, {"prompts"}, {"prompts"}, {"profiles"}, {"profiles", "details"}, {"sibling"}} {
|
|
destination := append([]string(nil), test.prefix...)
|
|
for _, segment := range suffix {
|
|
destination = append(destination, segment)
|
|
}
|
|
wantDestinations = append(wantDestinations, configkit.Keys(destination...))
|
|
}
|
|
sources := test.result.Sources()
|
|
if len(sources) != len(wantPaths) {
|
|
t.Fatalf("sources = %#v, want %d", sources, len(wantPaths))
|
|
}
|
|
for index, source := range sources {
|
|
if filepath.Base(source.Path) != wantPaths[index] || source.Path != wantPhysicalPaths[index] || source.Destination.String() != wantDestinations[index].String() {
|
|
t.Errorf("source %d path/destination = (%q, %q), want physical path %q and destination %q", index, source.Path, source.Destination.String(), wantPhysicalPaths[index], wantDestinations[index].String())
|
|
}
|
|
if source.ID != fmt.Sprintf("source-%d", index+1) || source.Kind != configkit.SourceKindFile {
|
|
t.Errorf("source %d identity/kind = (%q, %q)", index, source.ID, source.Kind)
|
|
}
|
|
if wantImportParents[index] == "" {
|
|
if source.ImportedFrom != nil {
|
|
t.Errorf("entry source unexpectedly has an import location: %#v", source.ImportedFrom)
|
|
}
|
|
} else if source.ImportedFrom == nil || source.ImportedFrom.SourceID != wantImportParents[index] || source.ImportedFrom.Line == 0 || source.ImportedFrom.Column == 0 {
|
|
t.Errorf("source %d import location = %#v, want declaration in %s", index, source.ImportedFrom, wantImportParents[index])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFacadeSameFragmentMountsAtSeparateCollectionDestinations(t *testing.T) {
|
|
weatherDestination := configkit.Keys("collections", "weather", "prompts")
|
|
homeDestination := configkit.Keys("collections", "homewatch", "prompts")
|
|
result, err := configkit.LoadLayered(
|
|
configkit.Input{Source: configkit.NoSource()},
|
|
configkit.LayeredOptions[map[string]any]{Underlays: []configkit.Underlay{{Inputs: []configkit.Input{
|
|
{Source: configkit.Bytes("weather", []byte("summarize.alerts: {title: weather}\n")), Destination: weatherDestination},
|
|
{Source: configkit.Bytes("homewatch", []byte("summarize.alerts: {title: home}\n")), Destination: homeDestination},
|
|
}}}},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("LoadLayered: %v", err)
|
|
}
|
|
for _, test := range []struct {
|
|
path []string
|
|
want string
|
|
}{
|
|
{path: []string{"collections", "weather", "prompts", "summarize.alerts", "title"}, want: "weather"},
|
|
{path: []string{"collections", "homewatch", "prompts", "summarize.alerts", "title"}, want: "home"},
|
|
} {
|
|
if got := facadeDestinationValue(t, result.Config, test.path...); got != test.want {
|
|
t.Errorf("value at %v = %#v, want %q", test.path, got, test.want)
|
|
}
|
|
}
|
|
sources := result.Sources()
|
|
if len(sources) != 2 || sources[0].Destination.String() != weatherDestination.String() || sources[1].Destination.String() != homeDestination.String() {
|
|
t.Fatalf("independent collection source placements = %#v", sources)
|
|
}
|
|
}
|
|
|
|
type destinationCountingReader struct{ reads int }
|
|
|
|
func (reader *destinationCountingReader) Read([]byte) (int, error) {
|
|
reader.reads++
|
|
return 0, io.EOF
|
|
}
|
|
|
|
func TestFacadeMountedMainAndUnderlayInspectionResourcesAndDiff(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
filesystem := configkit.NewFilesystem("resources", fstest.MapFS{
|
|
"assets/summary.md": &fstest.MapFile{Data: []byte("source-relative summary")},
|
|
})
|
|
context := &configkit.FilesystemContext{Root: ".", BaseDir: "assets", FS: &filesystem}
|
|
destination := configkit.Keys("catalog", "entries")
|
|
tokenPath := configkit.Keys("catalog", "entries", "item", "token")
|
|
titlePath := configkit.Keys("catalog", "entries", "item", "title")
|
|
var callbacks struct{ resolve, validate int }
|
|
load := func(token string) *configkit.Result[map[string]any] {
|
|
t.Helper()
|
|
main := configkit.Input{
|
|
Source: configkit.Bytes("main", []byte("item:\n title: main\n resource: summary.md\n token: "+token+"\n")),
|
|
Destination: destination, Filesystem: context,
|
|
}
|
|
underlay := configkit.Input{
|
|
Source: configkit.Bytes("underlay", []byte("item:\n title: lower\n retained: lower-value\n token: LOWER-TOKEN-CANARY\n")),
|
|
Destination: destination,
|
|
}
|
|
result, err := configkit.LoadLayered(main, configkit.LayeredOptions[map[string]any]{
|
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{underlay}}},
|
|
Definition: configkit.Definition[map[string]any]{
|
|
Resolve: func(configuration *map[string]any, resolution *configkit.Resolution) error {
|
|
callbacks.resolve++
|
|
resourcePath := configkit.Keys("catalog", "entries", "item", "resource")
|
|
input, ok := resolution.InputInfo(resourcePath)
|
|
if !ok || len(input.Origin.Locations) != 1 {
|
|
return fmt.Errorf("mounted resource input has no single source origin")
|
|
}
|
|
source, ok := resolution.Source(input.Origin.Locations[0].SourceID)
|
|
if !ok || source.Destination.String() != destination.String() {
|
|
return fmt.Errorf("resolution source did not retain its destination")
|
|
}
|
|
resourceContext, ok := resolution.SourceContext(source.ID)
|
|
if !ok {
|
|
return fmt.Errorf("resolution source context is unavailable")
|
|
}
|
|
data, err := fs.ReadFile(resourceContext.FS, path.Join(resourceContext.BaseDir, "summary.md"))
|
|
if err != nil {
|
|
return fmt.Errorf("read source-relative resource: %w", err)
|
|
}
|
|
entry := facadeDestinationMap(t, facadeDestinationMap(t, facadeDestinationMap(t, *configuration, "catalog"), "entries"), "item")
|
|
entry["resource_text"] = string(data)
|
|
resolution.Record(configkit.Keys("catalog", "entries", "item", "resource_text"), configkit.ChangeNormalization, "read source-relative summary", resourcePath)
|
|
return nil
|
|
},
|
|
Validate: func(*map[string]any, *configkit.Validation) { callbacks.validate++ },
|
|
Secret: func(candidate configkit.Path) bool { return candidate.String() == tokenPath.String() },
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("mounted load: %v", err)
|
|
}
|
|
return result
|
|
}
|
|
|
|
first := load("FIRST-TOKEN-CANARY")
|
|
second := load("SECOND-TOKEN-CANARY")
|
|
if callbacks.resolve != 2 || callbacks.validate != 2 {
|
|
t.Fatalf("lifecycle callbacks = %#v", callbacks)
|
|
}
|
|
for _, result := range []*configkit.Result[map[string]any]{first, second} {
|
|
entry := facadeDestinationMap(t, facadeDestinationMap(t, facadeDestinationMap(t, result.Config, "catalog"), "entries"), "item")
|
|
if entry["title"] != "main" || entry["retained"] != "lower-value" || entry["resource_text"] != "source-relative summary" {
|
|
t.Fatalf("mounted main/underlay configuration = %#v", entry)
|
|
}
|
|
sources := result.Sources()
|
|
if len(sources) != 2 || sources[0].Destination.String() != destination.String() || sources[1].Destination.String() != destination.String() {
|
|
t.Fatalf("public source destinations = %#v", sources)
|
|
}
|
|
resource, ok := result.SourceContext(sources[0].ID)
|
|
if !ok || resource.BaseDir != "assets" {
|
|
t.Fatalf("completed source context = %#v, %v", resource, ok)
|
|
}
|
|
data, err := fs.ReadFile(resource.FS, path.Join(resource.BaseDir, "summary.md"))
|
|
if err != nil || string(data) != "source-relative summary" {
|
|
t.Fatalf("completed relative resource = %q, %v", data, err)
|
|
}
|
|
if info, ok := result.Metadata().Lookup(titlePath); !ok || info.Origin.Layer != configkit.LayerBase {
|
|
t.Fatalf("mounted title metadata = %#v, %v", info, ok)
|
|
}
|
|
if history := result.Metadata().History(titlePath); len(history) < 2 {
|
|
t.Fatalf("mounted title history = %#v", history)
|
|
}
|
|
if explanation, ok := result.Explain(tokenPath); !ok || !explanation.Effective.Redacted {
|
|
t.Fatalf("mounted secret explanation = %#v, %v", explanation, ok)
|
|
}
|
|
var effective, explanation bytes.Buffer
|
|
if err := result.WriteEffective(&effective); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := result.WriteExplanation(&explanation, tokenPath); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
output := effective.String() + explanation.String()
|
|
for _, canary := range []string{"FIRST-TOKEN-CANARY", "SECOND-TOKEN-CANARY", "LOWER-TOKEN-CANARY"} {
|
|
if strings.Contains(output, canary) {
|
|
t.Fatalf("public inspection leaked %q: %s", canary, output)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mutating published records and query copies must not alter the retained result.
|
|
returnedSources := first.Sources()
|
|
returnedSources[0].Destination = configkit.Keys("mutated")
|
|
if got := first.Sources()[0].Destination.String(); got != destination.String() {
|
|
t.Fatalf("source destination changed after caller mutation: %q", got)
|
|
}
|
|
info, ok := first.Metadata().Lookup(titlePath)
|
|
if !ok {
|
|
t.Fatal("mounted metadata lookup failed")
|
|
}
|
|
info.Origin.Locations[0].SourceID = "mutated"
|
|
if fresh, ok := first.Metadata().Lookup(titlePath); !ok || fresh.Origin.Locations[0].SourceID == "mutated" {
|
|
t.Fatalf("metadata copy mutation escaped: %#v, %v", fresh, ok)
|
|
}
|
|
history := first.Metadata().History(titlePath)
|
|
if len(history) == 0 {
|
|
t.Fatal("mounted history is empty")
|
|
}
|
|
history[0].Path = configkit.Keys("mutated")
|
|
if fresh := first.Metadata().History(titlePath); len(fresh) == 0 || fresh[0].Path.String() != titlePath.String() {
|
|
t.Fatalf("history copy mutation escaped: %#v", fresh)
|
|
}
|
|
|
|
differences, err := configkit.Diff(first, second)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(differences) != 1 || differences[0].Path.String() != tokenPath.String() ||
|
|
!differences[0].Left.Redacted || !differences[0].Right.Redacted ||
|
|
strings.Contains(differences[0].Left.YAML+differences[0].Right.YAML, "TOKEN-CANARY") {
|
|
t.Fatalf("public mounted secret diff = %#v", differences)
|
|
}
|
|
if callbacks.resolve != 2 || callbacks.validate != 2 {
|
|
t.Fatalf("inspection reran lifecycle callbacks: %#v", callbacks)
|
|
}
|
|
}
|
|
|
|
func TestFacadeFailedLoadExposesDetachedSourceDestination(t *testing.T) {
|
|
type configuration struct {
|
|
Tenant struct {
|
|
Count int `yaml:"count"`
|
|
} `yaml:"tenant"`
|
|
}
|
|
destination := configkit.Keys("tenant")
|
|
_, err := configkit.LoadLayered[configuration](configkit.Input{
|
|
Source: configkit.Bytes("invalid", []byte("count: not-an-integer\n")), Destination: destination,
|
|
})
|
|
loadError, ok := err.(*configkit.LoadError)
|
|
if !ok {
|
|
t.Fatalf("failed load error = %T %v", err, err)
|
|
}
|
|
sources := loadError.Sources()
|
|
if len(sources) != 1 || sources[0].Destination.String() != destination.String() {
|
|
t.Fatalf("failed source destinations = %#v", sources)
|
|
}
|
|
sources[0].Destination = configkit.Keys("caller-change")
|
|
if fresh := loadError.Sources(); len(fresh) != 1 || fresh[0].Destination.String() != destination.String() {
|
|
t.Fatalf("failed source destination was not detached: %#v", fresh)
|
|
}
|
|
}
|
|
|
|
func TestFacadeRelativeImportFailuresRetainSourceRecordsAndCoordinates(t *testing.T) {
|
|
directory := t.TempDir()
|
|
write := func(name, contents string) string {
|
|
t.Helper()
|
|
filename := filepath.Join(directory, name)
|
|
if err := os.MkdirAll(filepath.Dir(filename), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filename, []byte(contents), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return filename
|
|
}
|
|
|
|
t.Run("imported parse failure retains effective source destination", func(t *testing.T) {
|
|
mainPath := write("parse/main.yml", "configkit:\n imports:\n - path: child.yml\n destination: [tenant]\n")
|
|
childPath := write("parse/child.yml", "count: [unterminated\n")
|
|
_, err := configkit.LoadLayered[map[string]any](configkit.Input{
|
|
Source: configkit.File(mainPath), Imports: true, Destination: configkit.Keys("mount"),
|
|
})
|
|
var loadError *configkit.LoadError
|
|
if !errors.As(err, &loadError) {
|
|
t.Fatalf("load error = %T %v", err, err)
|
|
}
|
|
diagnostics := loadError.Diagnostics()
|
|
if len(diagnostics) != 1 || diagnostics[0].Code != configkit.CodeYAMLInvalid || diagnostics[0].Primary == nil || diagnostics[0].Primary.SourceID != "source-2" {
|
|
t.Fatalf("imported parse diagnostic = %#v", diagnostics)
|
|
}
|
|
sources := loadError.Sources()
|
|
if len(sources) != 2 || sources[0].Destination.String() != "/mount" || sources[1].Destination.String() != "/mount/tenant" || sources[1].Path != childPath {
|
|
t.Fatalf("imported parse failure sources = %#v", sources)
|
|
}
|
|
})
|
|
|
|
t.Run("directive failure stays source local", func(t *testing.T) {
|
|
mainPath := write("directive/main.yml", "configkit:\n imports:\n - path: parent.yml\n destination: [tenant]\n")
|
|
parentPath := write("directive/parent.yml", "configkit:\n imports:\n - path: details.yml\n destination: [3]\n")
|
|
_, err := configkit.LoadLayered[map[string]any](configkit.Input{
|
|
Source: configkit.File(mainPath), Imports: true, Destination: configkit.Keys("mount"),
|
|
})
|
|
var loadError *configkit.LoadError
|
|
if !errors.As(err, &loadError) {
|
|
t.Fatalf("load error = %T %v", err, err)
|
|
}
|
|
diagnostics := loadError.Diagnostics()
|
|
wantPath := configkit.Keys("configkit", "imports").Index(0).Key("destination").Index(0)
|
|
if len(diagnostics) != 1 || diagnostics[0].Code != configkit.CodeImportInvalid || !reflect.DeepEqual(diagnostics[0].Path, wantPath) || diagnostics[0].Primary == nil || diagnostics[0].Primary.SourceID != "source-2" {
|
|
t.Fatalf("import directive diagnostic = %#v", diagnostics)
|
|
}
|
|
sources := loadError.Sources()
|
|
if len(sources) != 2 || sources[0].Destination.String() != "/mount" || sources[1].Destination.String() != "/mount/tenant" || sources[1].Path != parentPath {
|
|
t.Fatalf("directive failure sources = %#v", sources)
|
|
}
|
|
})
|
|
|
|
t.Run("composition failure reports effective path and all sources", func(t *testing.T) {
|
|
mainPath := write("compose/main.yml", "configkit:\n imports:\n - path: left.yml\n destination: [tenant]\n - path: right.yml\n destination: [tenant]\n")
|
|
write("compose/left.yml", "value: same\n")
|
|
write("compose/right.yml", "value: same\n")
|
|
_, err := configkit.LoadLayered[map[string]any](configkit.Input{
|
|
Source: configkit.File(mainPath), Imports: true, Destination: configkit.Keys("mount"),
|
|
})
|
|
var loadError *configkit.LoadError
|
|
if !errors.As(err, &loadError) {
|
|
t.Fatalf("load error = %T %v", err, err)
|
|
}
|
|
diagnostics := loadError.Diagnostics()
|
|
if len(diagnostics) != 1 || diagnostics[0].Code != configkit.CodeComposeConflict || diagnostics[0].Path.String() != "/mount/tenant/value" {
|
|
t.Fatalf("composition diagnostic = %#v", diagnostics)
|
|
}
|
|
sources := loadError.Sources()
|
|
if len(sources) != 3 || sources[0].Destination.String() != "/mount" || sources[1].Destination.String() != "/mount/tenant" || sources[2].Destination.String() != "/mount/tenant" {
|
|
t.Fatalf("composition failure sources = %#v", sources)
|
|
}
|
|
})
|
|
|
|
t.Run("version failure reports effective path and imported source", func(t *testing.T) {
|
|
mainPath := write("version/main.yml", "configkit:\n imports:\n - path: child.yml\n destination: [tenant]\n")
|
|
childPath := write("version/child.yml", "version: forbidden\n")
|
|
_, err := configkit.LoadLayered[map[string]any](configkit.Input{
|
|
Source: configkit.File(mainPath), Imports: true, Destination: configkit.Keys("mount"),
|
|
}, configkit.LayeredOptions[map[string]any]{
|
|
Definition: configkit.Definition[map[string]any]{Version: &configkit.VersionRule{
|
|
Path: configkit.Keys("mount", "tenant", "version"),
|
|
Check: func(configkit.Version, *configkit.Validation) error { return nil },
|
|
}},
|
|
})
|
|
var loadError *configkit.LoadError
|
|
if !errors.As(err, &loadError) {
|
|
t.Fatalf("load error = %T %v", err, err)
|
|
}
|
|
diagnostics := loadError.Diagnostics()
|
|
if len(diagnostics) != 1 || diagnostics[0].Code != configkit.CodeImportVersion || diagnostics[0].Path.String() != "/mount/tenant/version" || diagnostics[0].Primary == nil || diagnostics[0].Primary.SourceID != "source-2" {
|
|
t.Fatalf("version diagnostic = %#v", diagnostics)
|
|
}
|
|
sources := loadError.Sources()
|
|
if len(sources) != 2 || sources[0].Destination.String() != "/mount" || sources[1].Destination.String() != "/mount/tenant" || sources[1].Path != childPath {
|
|
t.Fatalf("version failure sources = %#v", sources)
|
|
}
|
|
})
|
|
|
|
t.Run("typed validation reports effective path and authored location", func(t *testing.T) {
|
|
type configuration struct {
|
|
Mount struct {
|
|
Tenant struct {
|
|
Count int `yaml:"count"`
|
|
} `yaml:"tenant"`
|
|
} `yaml:"mount"`
|
|
}
|
|
mainPath := write("typed/main.yml", "configkit:\n imports:\n - path: child.yml\n destination: [tenant]\n")
|
|
childPath := write("typed/child.yml", "count: invalid\n")
|
|
_, err := configkit.LoadLayered[configuration](configkit.Input{
|
|
Source: configkit.File(mainPath), Imports: true, Destination: configkit.Keys("mount"),
|
|
})
|
|
var loadError *configkit.LoadError
|
|
if !errors.As(err, &loadError) {
|
|
t.Fatalf("load error = %T %v", err, err)
|
|
}
|
|
diagnostics := loadError.Diagnostics()
|
|
if len(diagnostics) != 1 || diagnostics[0].Code != configkit.CodeDecodeType || diagnostics[0].Path.String() != "/mount/tenant/count" || diagnostics[0].Primary == nil || diagnostics[0].Primary.SourceID != "source-2" || diagnostics[0].Primary.Line != 1 {
|
|
t.Fatalf("typed validation diagnostic = %#v", diagnostics)
|
|
}
|
|
sources := loadError.Sources()
|
|
if len(sources) != 2 || sources[0].Destination.String() != "/mount" || sources[1].Destination.String() != "/mount/tenant" || sources[1].Path != childPath {
|
|
t.Fatalf("typed validation sources = %#v", sources)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFacadeDestinationMappingFailurePaths(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cycle := map[string]any{}
|
|
cycle["self"] = cycle
|
|
for _, fixture := range []struct {
|
|
name string
|
|
values map[string]any
|
|
path func(configkit.Path) configkit.Path
|
|
}{
|
|
{
|
|
name: "unsupported nested sequence value",
|
|
values: map[string]any{"a/b": []any{map[string]any{"~": func() {}}}},
|
|
path: func(base configkit.Path) configkit.Path { return base.Key("a/b").Index(0).Key("~") },
|
|
},
|
|
{
|
|
name: "cyclic value",
|
|
values: map[string]any{"item": cycle},
|
|
path: func(base configkit.Path) configkit.Path { return base.Key("item").Key("self") },
|
|
},
|
|
{
|
|
name: "constructor failure keeps source coordinates",
|
|
path: func(configkit.Path) configkit.Path { return configkit.Path{} },
|
|
},
|
|
} {
|
|
t.Run(fixture.name, func(t *testing.T) {
|
|
for _, role := range []string{"root", "mounted main", "mounted underlay"} {
|
|
t.Run(role, func(t *testing.T) {
|
|
var destination configkit.Path
|
|
if role != "root" {
|
|
destination = configkit.Keys("catalog", "entries")
|
|
}
|
|
wantPath := fixture.path(destination)
|
|
input := configkit.Input{Source: configkit.Mapping("invalid mapping", fixture.values), Destination: destination}
|
|
var result *configkit.Result[map[string]any]
|
|
var err error
|
|
if role == "mounted underlay" {
|
|
result, err = configkit.LoadLayered(configkit.Input{Source: configkit.NoSource()}, configkit.LayeredOptions[map[string]any]{
|
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{input}}},
|
|
})
|
|
} else {
|
|
result, err = configkit.LoadLayered[map[string]any](input)
|
|
}
|
|
var loadError *configkit.LoadError
|
|
if result != nil || !errors.As(err, &loadError) {
|
|
t.Fatalf("failed load = %#v, %v", result, err)
|
|
}
|
|
diagnostics := loadError.Diagnostics()
|
|
if len(diagnostics) != 1 {
|
|
t.Fatalf("diagnostics = %#v", diagnostics)
|
|
}
|
|
diagnostic := diagnostics[0]
|
|
if diagnostic.Code != configkit.CodeSourceInvalid || !reflect.DeepEqual(diagnostic.Path, wantPath) {
|
|
t.Fatalf("diagnostic = %#v; want source.invalid at %q", diagnostic, wantPath.String())
|
|
}
|
|
if diagnostic.Primary == nil || *diagnostic.Primary != (configkit.Location{SourceID: "source-1"}) || len(diagnostic.Related) != 0 {
|
|
t.Fatalf("mapping source locations = %#v, %#v", diagnostic.Primary, diagnostic.Related)
|
|
}
|
|
sources := loadError.Sources()
|
|
if len(sources) != 1 || sources[0].ID != "source-1" || sources[0].Destination.String() != destination.String() {
|
|
t.Fatalf("error sources = %#v", sources)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func facadeDestinationMap(t *testing.T, value map[string]any, keys ...string) map[string]any {
|
|
t.Helper()
|
|
for _, key := range keys {
|
|
child, ok := value[key].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("%q = %#v, want mapping", key, value[key])
|
|
}
|
|
value = child
|
|
}
|
|
return value
|
|
}
|
|
|
|
func facadeDestinationValue(t *testing.T, value map[string]any, keys ...string) any {
|
|
t.Helper()
|
|
return facadeDestinationMap(t, value, keys[:len(keys)-1]...)[keys[len(keys)-1]]
|
|
}
|