662 lines
29 KiB
Go
662 lines
29 KiB
Go
package configkit_test
|
|
|
|
import (
|
|
"bytes"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"testing/fstest"
|
|
|
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
|
)
|
|
|
|
func TestFacadeNoSourcePreservesAbsenceWithSequencePolicy(t *testing.T) {
|
|
t.Parallel()
|
|
for _, defaults := range []bool{false, true} {
|
|
for _, registered := range []bool{false, true} {
|
|
definition := configkit.Definition[map[string]any]{}
|
|
if defaults {
|
|
definition.Defaults = func() map[string]any { return map[string]any{"fallback": true} }
|
|
}
|
|
if registered {
|
|
definition.SequenceCompositions = []configkit.SequenceComposition{{Path: configkit.Keys("records"), IdentityField: "id"}}
|
|
}
|
|
ordinary, err := configkit.Load(configkit.NoSource(), configkit.Options[map[string]any]{Definition: definition})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
layered, err := configkit.LoadLayered[map[string]any](configkit.Input{Source: configkit.NoSource()}, configkit.LayeredOptions[map[string]any]{Definition: definition})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(ordinary.Config, layered.Config) {
|
|
t.Fatal("NoSource values differ between entry points")
|
|
}
|
|
for _, result := range []*configkit.Result[map[string]any]{ordinary, layered} {
|
|
if len(result.Sources()) != 0 || result.Metadata().IsExplicit(configkit.Keys()) {
|
|
t.Fatal("NoSource contributed an explicit root")
|
|
}
|
|
info, ok := result.Metadata().Lookup(configkit.Keys())
|
|
if !ok || info.Origin.Layer != configkit.LayerDefault {
|
|
t.Fatalf("NoSource root origin = %#v, %t", info, ok)
|
|
}
|
|
}
|
|
empty, err := configkit.Load(configkit.Mapping("empty", map[string]any{}), configkit.Options[map[string]any]{Definition: definition})
|
|
if err != nil || !empty.Metadata().IsExplicit(configkit.Keys()) {
|
|
t.Fatal("real empty mapping lost its presence")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFacadeItemHelperHistoryPreservesScheduledPlacement(t *testing.T) {
|
|
t.Parallel()
|
|
filesystem := configkit.NewFilesystem("history", fstest.MapFS{
|
|
"main.yml": {Data: []byte("records: [{id: authored}]\nconfigkit: {imports: [{path: one.yml, destination: [records], placement: sequence-item}, {path: two.yml, destination: [records], placement: sequence-item}]}\n")},
|
|
"one.yml": {Data: []byte("id: one\nconfigkit: {imports: [{path: helper-one.yml}, {path: child.yml, destination: [children], placement: sequence-item}]}\n")},
|
|
"two.yml": {Data: []byte("id: two\nconfigkit: {imports: [{path: helper-two.yml}]}\n")},
|
|
"helper-one.yml": {Data: []byte("text: first helper\n")},
|
|
"helper-two.yml": {Data: []byte("text: second helper\n")},
|
|
"child.yml": {Data: []byte("id: child\nconfigkit: {imports: [{path: child-helper.yml}]}\n")},
|
|
"child-helper.yml": {Data: []byte("text: nested helper\n")},
|
|
})
|
|
result, err := configkit.Load(filesystem.File("main.yml"), configkit.Options[map[string]any]{
|
|
Imports: true, Definition: configkit.Definition[map[string]any]{SequenceCompositions: []configkit.SequenceComposition{{Path: configkit.Keys("records"), IdentityField: "id"}}},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sourceFiles := make(map[string]string)
|
|
for _, source := range result.Sources() {
|
|
sourceFiles[source.ID] = source.Path
|
|
}
|
|
for _, test := range []struct {
|
|
path configkit.Path
|
|
file, text string
|
|
}{
|
|
{configkit.Keys("records").Index(0).Key("text"), "helper-one.yml", "first helper\n"},
|
|
{configkit.Keys("records").Index(1).Key("text"), "helper-two.yml", "second helper\n"},
|
|
{configkit.Keys("records").Index(0).Key("children").Index(0).Key("text"), "child-helper.yml", "nested helper\n"},
|
|
} {
|
|
explanation, ok := result.Explain(test.path)
|
|
if !ok || explanation.Effective.YAML != test.text || len(explanation.History) != 1 {
|
|
t.Fatalf("helper history at %s = %#v", test.path.String(), explanation)
|
|
}
|
|
locations := explanation.History[0].Origin.Locations
|
|
if len(locations) != 1 || sourceFiles[locations[0].SourceID] != test.file {
|
|
t.Fatalf("helper origin at %s = %#v", test.path.String(), locations)
|
|
}
|
|
info, ok := result.Metadata().Lookup(test.path)
|
|
if !ok || len(info.Origin.Locations) != 1 || info.Origin.Locations[0].SourceID != locations[0].SourceID {
|
|
t.Fatal("metadata and explanation disagree")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFacadeRegisteredReplacementExplainsRemovedDescendants(t *testing.T) {
|
|
t.Parallel()
|
|
for _, test := range []struct {
|
|
name string
|
|
upper []any
|
|
path configkit.Path
|
|
}{
|
|
{"shorter", []any{map[string]any{"id": "main"}}, configkit.Keys("records").Index(1).Key("id")},
|
|
{"empty", []any{}, configkit.Keys("records").Index(0).Key("id")},
|
|
{"removed field", []any{map[string]any{"id": "main"}}, configkit.Keys("records").Index(0).Key("text")},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
result, err := configkit.LoadLayered[map[string]any](configkit.Input{Source: configkit.Mapping("main", map[string]any{"records": test.upper})}, configkit.LayeredOptions[map[string]any]{
|
|
Definition: configkit.Definition[map[string]any]{SequenceCompositions: []configkit.SequenceComposition{{Path: configkit.Keys("records"), IdentityField: "id"}}},
|
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Mapping("lower", map[string]any{"records": []any{map[string]any{"id": "one", "text": "old"}, map[string]any{"id": "two"}}})}}}},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
explanation, ok := result.Explain(test.path)
|
|
if !ok || explanation.Effective.Present || len(explanation.History) < 2 {
|
|
t.Fatalf("removal not explained: %#v", explanation)
|
|
}
|
|
last := explanation.History[len(explanation.History)-1]
|
|
if !last.Before.Present || last.After.Present || last.Origin.Layer != configkit.LayerBase || len(last.Origin.Locations) != 1 || last.Origin.Locations[0].SourceID != result.Sources()[0].ID {
|
|
t.Fatalf("wrong removal transition/origin: %#v", last)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFacadeItemConflictDiagnosticsFollowEnclosingIndexes(t *testing.T) {
|
|
t.Parallel()
|
|
filesystem := configkit.NewFilesystem("conflicts", fstest.MapFS{
|
|
"main.yml": {Data: []byte("records: [{id: authored}]\nconfigkit: {imports: [{path: good.yml, destination: [records], placement: sequence-item}, {path: bad.yml, destination: [records], placement: sequence-item}]}\n")},
|
|
"group.yml": {Data: []byte("records: [{id: authored}]\nconfigkit: {imports: [{path: bad.yml, destination: [records], placement: sequence-item}]}\n")},
|
|
"nested.yml": {Data: []byte("records: [{id: authored}]\nconfigkit: {imports: [{path: parent.yml, destination: [records], placement: sequence-item}]}\n")},
|
|
"parent.yml": {Data: []byte("id: parent\nchildren: [{id: authored-child}]\nconfigkit: {imports: [{path: bad.yml, destination: [children], placement: sequence-item}]}\n")},
|
|
"good.yml": {Data: []byte("id: good\n")},
|
|
"bad.yml": {Data: []byte("id: bad\nconfigkit: {imports: [{path: helper.yml}]}\n")},
|
|
"helper.yml": {Data: []byte("id: duplicate-field\n")},
|
|
})
|
|
definition := configkit.Definition[map[string]any]{SequenceCompositions: []configkit.SequenceComposition{{Path: configkit.Keys("records"), IdentityField: "id"}}}
|
|
for _, test := range []struct {
|
|
name, file, path string
|
|
grouped, itemMain bool
|
|
}{
|
|
{"preceding authored and imported items", "main.yml", "/records/0/id", false, false},
|
|
{"nested item", "nested.yml", "/records/0/children/0/id", false, false},
|
|
{"preceding grouped input", "group.yml", "/records/1/id", true, false},
|
|
{"item main source", "bad.yml", "/records/0/id", false, true},
|
|
{"grouped item source", "bad.yml", "/records/1/id", true, true},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
var err error
|
|
input := configkit.Input{Source: filesystem.File(test.file), Imports: true}
|
|
if test.itemMain {
|
|
input.Destination = configkit.Keys("records")
|
|
input.PlacementMode = configkit.PlacementModeSequenceItem
|
|
}
|
|
if test.grouped {
|
|
_, err = configkit.LoadLayered[map[string]any](configkit.Input{Source: configkit.NoSource()}, configkit.LayeredOptions[map[string]any]{
|
|
Definition: definition, Underlays: []configkit.Underlay{{Inputs: []configkit.Input{
|
|
{Source: configkit.Mapping("prior", map[string]any{"records": []any{map[string]any{"id": "prior"}}})},
|
|
input,
|
|
}}},
|
|
})
|
|
} else {
|
|
_, err = configkit.Load(input.Source, configkit.Options[map[string]any]{Definition: definition, Imports: true, Destination: input.Destination, PlacementMode: input.PlacementMode})
|
|
}
|
|
loadError, ok := err.(*configkit.LoadError)
|
|
if !ok || len(loadError.Diagnostics()) != 1 {
|
|
t.Fatalf("expected helper conflict: %v", err)
|
|
}
|
|
diagnostic := loadError.Diagnostics()[0]
|
|
if diagnostic.Code != configkit.CodeComposeConflict || diagnostic.Path.String() != test.path {
|
|
t.Fatalf("diagnostic = %s %s, want %s", diagnostic.Code, diagnostic.Path.String(), test.path)
|
|
}
|
|
files := make(map[string]string)
|
|
for _, source := range loadError.Sources() {
|
|
files[source.ID] = source.Path
|
|
}
|
|
locations := append([]configkit.Location(nil), diagnostic.Related...)
|
|
if diagnostic.Primary != nil {
|
|
locations = append(locations, *diagnostic.Primary)
|
|
}
|
|
gotFiles := make(map[string]bool, len(locations))
|
|
for _, location := range locations {
|
|
gotFiles[files[location.SourceID]] = location.Line == 1 && location.Column == 5
|
|
}
|
|
if len(locations) != 2 || !gotFiles["bad.yml"] || !gotFiles["helper.yml"] {
|
|
t.Fatalf("conflict lost physical source locations: diagnostic=%#v sources=%#v", diagnostic, files)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFacadeSequencePlacementControlsAcrossLoadEntryPoints(t *testing.T) {
|
|
definition := configkit.Definition[map[string]any]{
|
|
SequenceCompositions: []configkit.SequenceComposition{{Path: configkit.Keys("entries"), IdentityField: "id"}},
|
|
}
|
|
ordinary, err := configkit.Load(configkit.Mapping("ordinary", map[string]any{"id": "one"}), configkit.Options[map[string]any]{
|
|
Definition: definition, Destination: configkit.Keys("entries"), PlacementMode: configkit.PlacementModeSequenceItem,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ordinary item load: %v", err)
|
|
}
|
|
if got := ordinary.Config["entries"].([]any); len(got) != 1 || got[0].(map[string]any)["id"] != "one" {
|
|
t.Fatalf("ordinary item placement = %#v", ordinary.Config)
|
|
}
|
|
if sources := ordinary.Sources(); len(sources) != 1 || sources[0].PlacementMode != configkit.PlacementModeSequenceItem || sources[0].Destination.String() != "/entries" || sources[0].DestinationUnresolved {
|
|
t.Fatalf("ordinary item source = %#v", sources)
|
|
}
|
|
|
|
layered, err := configkit.LoadLayered[map[string]any](configkit.Input{Source: configkit.NoSource()}, configkit.LayeredOptions[map[string]any]{
|
|
Definition: definition,
|
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{
|
|
{Source: configkit.Mapping("first", map[string]any{"id": "first"}), Destination: configkit.Keys("entries"), PlacementMode: configkit.PlacementModeSequenceItem},
|
|
{Source: configkit.Mapping("second", map[string]any{"id": "second"}), Destination: configkit.Keys("entries"), PlacementMode: configkit.PlacementModeSequenceItem},
|
|
}}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("layered item load: %v", err)
|
|
}
|
|
items := layered.Config["entries"].([]any)
|
|
if len(items) != 2 || items[0].(map[string]any)["id"] != "first" || items[1].(map[string]any)["id"] != "second" {
|
|
t.Fatalf("layered item placement = %#v", layered.Config)
|
|
}
|
|
for _, source := range layered.Sources() {
|
|
if source.PlacementMode != configkit.PlacementModeSequenceItem || source.Destination.String() != "/entries" || source.DestinationUnresolved {
|
|
t.Errorf("layered item source = %#v", source)
|
|
}
|
|
}
|
|
|
|
precedence, err := configkit.LoadLayered[map[string]any](configkit.Input{Source: configkit.Mapping("main", map[string]any{
|
|
"entries": []any{map[string]any{"id": "main"}},
|
|
})}, configkit.LayeredOptions[map[string]any]{
|
|
Definition: definition,
|
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{
|
|
Source: configkit.Mapping("lower", map[string]any{"id": "lower"}), Destination: configkit.Keys("entries"), PlacementMode: configkit.PlacementModeSequenceItem,
|
|
}}}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("registered sequence precedence load: %v", err)
|
|
}
|
|
if got := precedence.Config["entries"].([]any); len(got) != 1 || got[0].(map[string]any)["id"] != "main" {
|
|
t.Fatalf("higher-priority list did not replace the lower sequence: %#v", precedence.Config)
|
|
}
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
load func(configkit.Source) error
|
|
}{
|
|
{name: "ordinary", load: func(source configkit.Source) error {
|
|
_, loadErr := configkit.Load(source, configkit.Options[map[string]any]{PlacementMode: configkit.PlacementMode(9)})
|
|
return loadErr
|
|
}},
|
|
{name: "layered", load: func(source configkit.Source) error {
|
|
_, loadErr := configkit.LoadLayered[map[string]any](configkit.Input{Source: source, PlacementMode: configkit.PlacementMode(9)})
|
|
return loadErr
|
|
}},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
reader := &destinationCountingReader{}
|
|
err := test.load(configkit.Reader("unread", reader))
|
|
loadError, ok := err.(*configkit.LoadError)
|
|
if !ok || len(loadError.Diagnostics()) == 0 || loadError.Diagnostics()[0].Code != configkit.CodeOptionsInvalid || reader.reads != 0 {
|
|
t.Fatalf("invalid placement result = %v, reads = %d", err, reader.reads)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFacadeItemImportGlobComposesWithAuthoredSequence(t *testing.T) {
|
|
directory := t.TempDir()
|
|
mainPath := filepath.Join(directory, "main.yml")
|
|
if err := os.WriteFile(mainPath, []byte(`configkit:
|
|
imports:
|
|
- glob: records/*.yml
|
|
destination: [items]
|
|
placement: sequence-item
|
|
items:
|
|
- id: authored
|
|
name: authored item
|
|
`), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for name, id := range map[string]string{"b.yml": "imported-b", "a.yml": "imported-a"} {
|
|
if err := os.MkdirAll(filepath.Join(directory, "records"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(directory, "records", name), []byte("id: "+id+"\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
var callbackSources []configkit.SourceInfo
|
|
var callbackContexts []configkit.ResourceContext
|
|
result, err := configkit.Load(configkit.File(mainPath), configkit.Options[map[string]any]{
|
|
Imports: true,
|
|
Definition: configkit.Definition[map[string]any]{
|
|
SequenceCompositions: []configkit.SequenceComposition{{Path: configkit.Keys("items"), IdentityField: "id"}},
|
|
Resolve: func(_ *map[string]any, resolution *configkit.Resolution) error {
|
|
for _, id := range []string{"source-2", "source-3"} {
|
|
source, ok := resolution.Source(id)
|
|
if !ok {
|
|
t.Errorf("resolver could not query %s", id)
|
|
continue
|
|
}
|
|
context, found := resolution.SourceContext(id)
|
|
if !found || context.FS == nil || context.BaseDir != "records" {
|
|
t.Errorf("resolver context for %s = %#v, %v", id, context, found)
|
|
}
|
|
callbackSources = append(callbackSources, source)
|
|
callbackContexts = append(callbackContexts, context)
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("load item glob: %v", err)
|
|
}
|
|
items := result.Config["items"].([]any)
|
|
wantIDs := []string{"imported-a", "imported-b", "authored"}
|
|
for index, item := range items {
|
|
if got := item.(map[string]any)["id"]; got != wantIDs[index] {
|
|
t.Errorf("item %d ID = %#v, want %q", index, got, wantIDs[index])
|
|
}
|
|
}
|
|
if len(callbackSources) != 2 || len(callbackContexts) != 2 {
|
|
t.Fatalf("callback snapshots = %#v, contexts %#v", callbackSources, callbackContexts)
|
|
}
|
|
for index, source := range callbackSources {
|
|
if source.PlacementMode != configkit.PlacementModeSequenceItem || source.Destination.String() != "/items" || source.DestinationUnresolved {
|
|
t.Errorf("callback source = %#v", source)
|
|
}
|
|
if callbackContexts[index].FS == nil || callbackContexts[index].BaseDir != "records" {
|
|
t.Errorf("callback context = %#v", callbackContexts[index])
|
|
}
|
|
}
|
|
metadata := result.Metadata()
|
|
for index := 0; index < 2; index++ {
|
|
path := configkit.Keys("items").Index(index).Key("id")
|
|
info, ok := metadata.Lookup(path)
|
|
if !ok || !info.Explicit || len(info.Origin.Locations) != 1 || info.Origin.Locations[0].SourceID != []string{"source-2", "source-3"}[index] {
|
|
t.Errorf("shifted item origin at %s = %#v, %v", path.String(), info, ok)
|
|
}
|
|
}
|
|
if got := result.Sources(); len(got) != 3 || got[1].Destination.String() != "/items" || got[1].DestinationUnresolved {
|
|
t.Fatalf("final item sources = %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestFacadeItemDestinationFallbackEvidenceSurvivesFailures(t *testing.T) {
|
|
directory := t.TempDir()
|
|
mainPath := filepath.Join(directory, "main.yml")
|
|
helperPath := filepath.Join(directory, "helper.yml")
|
|
if err := os.WriteFile(mainPath, []byte("configkit: {imports: [{path: helper.yml, destination: [metadata]}]}\nid: main\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(helperPath, []byte("value: [unterminated\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err := configkit.Load(configkit.File(mainPath), configkit.Options[map[string]any]{
|
|
Imports: true, Destination: configkit.Keys("items"), PlacementMode: configkit.PlacementModeSequenceItem,
|
|
})
|
|
loadError, ok := err.(*configkit.LoadError)
|
|
if !ok {
|
|
t.Fatalf("early failure = %v", err)
|
|
}
|
|
sources := loadError.Sources()
|
|
if len(sources) != 2 || sources[0].Destination.String() != "/items" || sources[0].DestinationUnresolved ||
|
|
sources[0].PlacementMode != configkit.PlacementModeSequenceItem ||
|
|
sources[1].Destination.String() != "/items" || !sources[1].DestinationUnresolved ||
|
|
sources[1].PlacementMode != configkit.PlacementModeMapping {
|
|
t.Fatalf("early source snapshots = %#v", sources)
|
|
}
|
|
|
|
if err := os.WriteFile(helperPath, []byte("value: invalid-for-typed-schema\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
type item struct {
|
|
ID string `yaml:"id"`
|
|
Metadata struct {
|
|
Value int `yaml:"value"`
|
|
} `yaml:"metadata"`
|
|
}
|
|
type configuration struct {
|
|
Items []item `yaml:"items"`
|
|
}
|
|
_, err = configkit.Load(configkit.File(mainPath), configkit.Options[configuration]{
|
|
Imports: true, Destination: configkit.Keys("items"), PlacementMode: configkit.PlacementModeSequenceItem,
|
|
})
|
|
loadError, ok = err.(*configkit.LoadError)
|
|
if !ok {
|
|
t.Fatalf("post-placement failure = %v", err)
|
|
}
|
|
sources = loadError.Sources()
|
|
if len(sources) != 2 || sources[0].Destination.String() != "/items" || sources[0].DestinationUnresolved ||
|
|
sources[1].Destination.String() != "/items/0/metadata" || sources[1].DestinationUnresolved {
|
|
t.Fatalf("post-placement source snapshots = %#v", sources)
|
|
}
|
|
firstSnapshot := sources[1]
|
|
sources[1].Destination = configkit.Keys("mutated")
|
|
if again := loadError.Sources(); len(again) != 2 || again[1].Destination.String() != firstSnapshot.Destination.String() || again[1].DestinationUnresolved {
|
|
t.Fatalf("failure source snapshots were aliased: %#v", again)
|
|
}
|
|
|
|
var callback configkit.SourceInfo
|
|
_, err = configkit.Load(configkit.File(mainPath), configkit.Options[map[string]any]{
|
|
Imports: true, Destination: configkit.Keys("items"), PlacementMode: configkit.PlacementModeSequenceItem,
|
|
Definition: configkit.Definition[map[string]any]{Resolve: func(_ *map[string]any, resolution *configkit.Resolution) error {
|
|
callback, _ = resolution.Source("source-2")
|
|
return nil
|
|
}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("successful item load: %v", err)
|
|
}
|
|
if callback.Destination.String() != "/items/0/metadata" || callback.DestinationUnresolved || callback.PlacementMode != configkit.PlacementModeMapping {
|
|
t.Fatalf("callback source snapshot = %#v", callback)
|
|
}
|
|
}
|
|
|
|
func TestFacadeNestedItemFallbackUsesNearestResolvedContainer(t *testing.T) {
|
|
directory := t.TempDir()
|
|
mainPath := filepath.Join(directory, "main.yml")
|
|
childPath := filepath.Join(directory, "child.yml")
|
|
helperPath := filepath.Join(directory, "helper.yml")
|
|
if err := os.WriteFile(mainPath, []byte("configkit: {imports: [{path: child.yml, destination: [children], placement: sequence-item}]}\nid: parent\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(childPath, []byte("configkit: {imports: [{path: helper.yml, destination: [metadata]}]}\nid: child\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(helperPath, []byte("value: [unterminated\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err := configkit.Load(configkit.File(mainPath), configkit.Options[map[string]any]{
|
|
Imports: true, Destination: configkit.Keys("groups"), PlacementMode: configkit.PlacementModeSequenceItem,
|
|
})
|
|
loadError, ok := err.(*configkit.LoadError)
|
|
if !ok {
|
|
t.Fatalf("nested acquisition failure = %v", err)
|
|
}
|
|
sources := loadError.Sources()
|
|
if len(sources) != 3 {
|
|
t.Fatalf("nested source snapshots = %#v", sources)
|
|
}
|
|
if got := sources[0]; got.Destination.String() != "/groups" || got.DestinationUnresolved || got.PlacementMode != configkit.PlacementModeSequenceItem {
|
|
t.Errorf("outer item snapshot = %#v", got)
|
|
}
|
|
if got := sources[1]; got.Destination.String() != "/groups" || !got.DestinationUnresolved || got.PlacementMode != configkit.PlacementModeSequenceItem {
|
|
t.Errorf("nested item fallback = %#v", got)
|
|
}
|
|
if got := sources[2]; got.Destination.String() != "/groups" || !got.DestinationUnresolved || got.PlacementMode != configkit.PlacementModeMapping {
|
|
t.Errorf("nested helper fallback = %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestFacadeSequenceCompositionRegistrationPathIsDetached(t *testing.T) {
|
|
registrations := []configkit.SequenceComposition{{Path: configkit.Keys("records"), IdentityField: "id"}}
|
|
definition := configkit.Definition[map[string]any]{
|
|
SequenceCompositions: registrations,
|
|
Version: &configkit.VersionRule{
|
|
Path: configkit.Keys("version"),
|
|
Check: func(configkit.Version, *configkit.Validation) error {
|
|
registrations[0].Path = configkit.Keys("other")
|
|
registrations[0].IdentityField = "changed"
|
|
return nil
|
|
},
|
|
},
|
|
}
|
|
_, err := configkit.Load(configkit.Mapping("registration", map[string]any{
|
|
"version": "1",
|
|
"records": []any{map[string]any{"id": "same"}, map[string]any{"id": "same"}},
|
|
}), configkit.Options[map[string]any]{
|
|
Definition: definition, SourceVersionMode: configkit.SourceVersionModeIfPresent,
|
|
})
|
|
loadError, ok := err.(*configkit.LoadError)
|
|
if !ok {
|
|
t.Fatalf("duplicate registered identity = %v", err)
|
|
}
|
|
for _, diagnostic := range loadError.Diagnostics() {
|
|
if diagnostic.Code == configkit.CodeComposeConflict {
|
|
return
|
|
}
|
|
}
|
|
t.Fatalf("registration mutation changed the in-flight policy: %#v", loadError.Diagnostics())
|
|
}
|
|
|
|
func TestFacadeSourceInfoPlacementCopiesAndZeroValues(t *testing.T) {
|
|
zero := configkit.SourceInfo{}
|
|
if zero.PlacementMode != configkit.PlacementModeMapping || zero.DestinationUnresolved {
|
|
t.Fatalf("zero source info = %#v", zero)
|
|
}
|
|
loaded, err := configkit.Load[map[string]any](configkit.Mapping("mapping", map[string]any{"value": true}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sources := loaded.Sources()
|
|
if len(sources) != 1 || sources[0].PlacementMode != configkit.PlacementModeMapping || sources[0].DestinationUnresolved {
|
|
t.Fatalf("mapping source evidence = %#v", sources)
|
|
}
|
|
first := sources[0]
|
|
first.Destination = configkit.Keys("caller")
|
|
if again := loaded.Sources(); len(again) != 1 || again[0].Destination.String() != "" || again[0].DestinationUnresolved {
|
|
t.Fatalf("source evidence changed after caller mutation: %#v", again)
|
|
}
|
|
}
|
|
|
|
func TestFacadeSequenceReplacementKeepsTypedAndHistoricalEvidenceAligned(t *testing.T) {
|
|
type record struct {
|
|
ID string `yaml:"id"`
|
|
Count int `yaml:"count"`
|
|
Enabled bool `yaml:"enabled"`
|
|
Empty string `yaml:"empty"`
|
|
Nullable *string `yaml:"nullable"`
|
|
Token string `yaml:"token"`
|
|
Omitted string `yaml:"omitted"`
|
|
}
|
|
type configuration struct {
|
|
Entries []record `yaml:"entries"`
|
|
}
|
|
|
|
directory := t.TempDir()
|
|
mainPath := filepath.Join(directory, "main.yml")
|
|
underlayPath := filepath.Join(directory, "underlay.yml")
|
|
if err := os.WriteFile(mainPath, []byte(`entries:
|
|
- id: current
|
|
count: 0
|
|
enabled: false
|
|
empty: ""
|
|
nullable: null
|
|
token: CURRENT-SECRET-CANARY
|
|
`), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(underlayPath, []byte(`configkit:
|
|
imports:
|
|
- glob: records/*.yml
|
|
destination: [entries]
|
|
placement: sequence-item
|
|
`), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
recordsDir := filepath.Join(directory, "records")
|
|
if err := os.MkdirAll(recordsDir, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for name, body := range map[string]string{
|
|
"a.yml": "id: old-a\ntoken: OLD-SECRET-CANARY-A\n",
|
|
"b.yml": "id: old-b\ntoken: OLD-SECRET-CANARY-B\n",
|
|
} {
|
|
if err := os.WriteFile(filepath.Join(recordsDir, name), []byte(body), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
var callbackItems []configkit.SourceInfo
|
|
var callbackContexts []configkit.ResourceContext
|
|
result, err := configkit.LoadLayered(configkit.Input{Source: configkit.File(mainPath)}, configkit.LayeredOptions[configuration]{
|
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.File(underlayPath), Imports: true}}}},
|
|
Definition: configkit.Definition[configuration]{
|
|
SequenceCompositions: []configkit.SequenceComposition{{Path: configkit.Keys("entries"), IdentityField: "id"}},
|
|
Secret: func(path configkit.Path) bool {
|
|
return path.String() == "/entries/0/token" || path.String() == "/entries/1/token"
|
|
},
|
|
Resolve: func(value *configuration, resolution *configkit.Resolution) error {
|
|
for _, field := range []string{"count", "enabled", "empty", "nullable"} {
|
|
path := configkit.Keys("entries").Index(0).Key(field)
|
|
if !resolution.IsExplicit(path) {
|
|
t.Errorf("resolver did not see explicit %s", path.String())
|
|
}
|
|
}
|
|
if resolution.IsExplicit(configkit.Keys("entries").Index(0).Key("omitted")) {
|
|
t.Error("resolver treated an omitted field as explicit")
|
|
}
|
|
info, found := resolution.InputInfo(configkit.Keys("entries").Index(0).Key("count"))
|
|
if !found || info.Origin.Layer != configkit.LayerBase {
|
|
t.Errorf("resolver current-record input info = %#v, %v", info, found)
|
|
}
|
|
for _, id := range []string{"source-3", "source-4"} {
|
|
source, ok := resolution.Source(id)
|
|
if !ok {
|
|
t.Errorf("resolver could not query %s", id)
|
|
continue
|
|
}
|
|
context, contextOK := resolution.SourceContext(id)
|
|
if !contextOK || context.FS == nil || context.BaseDir != "records" {
|
|
t.Errorf("resolver context for %s = %#v, %v", id, context, contextOK)
|
|
}
|
|
callbackItems = append(callbackItems, source)
|
|
callbackContexts = append(callbackContexts, context)
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
if loadError, ok := err.(*configkit.LoadError); ok {
|
|
t.Fatalf("layered item replacement: %#v", loadError.Diagnostics())
|
|
}
|
|
t.Fatalf("layered item replacement: %v", err)
|
|
}
|
|
if len(result.Config.Entries) != 1 || result.Config.Entries[0].ID != "current" || result.Config.Entries[0].Count != 0 ||
|
|
result.Config.Entries[0].Enabled || result.Config.Entries[0].Empty != "" || result.Config.Entries[0].Nullable != nil {
|
|
t.Fatalf("typed replacement result = %#v", result.Config)
|
|
}
|
|
if len(callbackItems) != 2 || len(callbackContexts) != 2 {
|
|
t.Fatalf("callback sources = %#v; contexts = %#v", callbackItems, callbackContexts)
|
|
}
|
|
for _, source := range callbackItems {
|
|
if source.PlacementMode != configkit.PlacementModeSequenceItem || source.Destination.String() != "/entries" || source.DestinationUnresolved {
|
|
t.Errorf("callback item source = %#v", source)
|
|
}
|
|
}
|
|
metadata := result.Metadata()
|
|
for _, field := range []string{"count", "enabled", "empty", "nullable"} {
|
|
path := configkit.Keys("entries").Index(0).Key(field)
|
|
if info, ok := metadata.Lookup(path); !ok || !info.Explicit || (field == "nullable") != info.Null {
|
|
t.Errorf("explicit metadata at %s = %#v, %v", path.String(), info, ok)
|
|
}
|
|
}
|
|
if info, ok := metadata.Lookup(configkit.Keys("entries").Index(0).Key("omitted")); ok && info.Explicit {
|
|
t.Errorf("metadata marked an omitted zero-valued field explicit: %#v", info)
|
|
}
|
|
mainID := ""
|
|
for _, source := range result.Sources() {
|
|
if source.Path == mainPath {
|
|
mainID = source.ID
|
|
}
|
|
}
|
|
currentOrigin, ok := metadata.Lookup(configkit.Keys("entries").Index(0).Key("id"))
|
|
if !ok || len(currentOrigin.Origin.Locations) != 1 || currentOrigin.Origin.Locations[0].SourceID != mainID || mainID == "" {
|
|
t.Fatalf("replacement origin = %#v, main source %q", currentOrigin, mainID)
|
|
}
|
|
oldRecord, ok := result.Explain(configkit.Keys("entries").Index(1).Key("token"))
|
|
if !ok || oldRecord.Effective.Present {
|
|
t.Fatalf("removed appended item explanation = %#v, %v", oldRecord, ok)
|
|
}
|
|
maskedHistory := false
|
|
for _, change := range oldRecord.History {
|
|
maskedHistory = maskedHistory || change.Before.Redacted || change.After.Redacted
|
|
}
|
|
if !maskedHistory {
|
|
t.Fatal("removed appended item lost historical sensitivity")
|
|
}
|
|
var effective bytes.Buffer
|
|
if err := result.WriteEffective(&effective); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var explanation bytes.Buffer
|
|
if err := result.WriteExplanation(&explanation, configkit.Keys("entries").Index(1).Key("token")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, output := range []string{effective.String(), explanation.String()} {
|
|
for _, canary := range []string{"CURRENT-SECRET-CANARY", "OLD-SECRET-CANARY-A", "OLD-SECRET-CANARY-B"} {
|
|
if strings.Contains(output, canary) {
|
|
t.Fatalf("sensitive sequence value leaked: %s", output)
|
|
}
|
|
}
|
|
}
|
|
}
|