374 lines
16 KiB
Go
374 lines
16 KiB
Go
package configkit_test
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"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 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 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]]
|
|
}
|