894 lines
38 KiB
Go
894 lines
38 KiB
Go
package configkit_test
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"testing/fstest"
|
|
|
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
|
)
|
|
|
|
func TestFacadeSourceVersionEvidenceFollowsAssembledSequenceRanges(t *testing.T) {
|
|
t.Parallel()
|
|
for _, grouped := range []bool{false, true} {
|
|
index := 0
|
|
if grouped {
|
|
index = 1
|
|
}
|
|
for _, sensitivePath := range []string{"", fmt.Sprintf("/records/%d/version", index), fmt.Sprintf("/records/%d", index)} {
|
|
t.Run(fmt.Sprintf("grouped=%t/secret=%s", grouped, sensitivePath), func(t *testing.T) {
|
|
definition := configkit.Definition[map[string]any]{
|
|
SequenceCompositions: []configkit.SequenceComposition{{Path: configkit.Keys("records"), IdentityField: "id"}},
|
|
Version: &configkit.VersionRule{Path: configkit.Keys("records").Index(0).Key("version"), Check: func(configkit.Version, *configkit.Validation) error { return nil }},
|
|
Secret: func(path configkit.Path) bool { return sensitivePath != "" && path.String() == sensitivePath },
|
|
}
|
|
first := "records: [{id: first, version: first-version}]\n"
|
|
child := "records: [{id: child, version: PRIVATE-VERSION-CANARY}]\n"
|
|
var result *configkit.Result[map[string]any]
|
|
var err error
|
|
if grouped {
|
|
result, err = configkit.LoadLayered(configkit.Input{Source: configkit.Bytes("main", []byte(first))}, configkit.LayeredOptions[map[string]any]{
|
|
Definition: definition, SourceVersionMode: configkit.SourceVersionModeRequired,
|
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{
|
|
{Source: configkit.Bytes("first", []byte(first))}, {Source: configkit.Bytes("child", []byte(child))},
|
|
}}},
|
|
})
|
|
} else {
|
|
filesystem := configkit.NewFilesystem("versions", fstest.MapFS{
|
|
"main.yml": {Data: []byte("configkit: {imports: [{path: child.yml}]}\n" + first)},
|
|
"child.yml": {Data: []byte(child)},
|
|
})
|
|
result, err = configkit.Load(filesystem.File("main.yml"), configkit.Options[map[string]any]{
|
|
Definition: definition, Imports: true, SourceVersionMode: configkit.SourceVersionModeRequired,
|
|
})
|
|
}
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !grouped {
|
|
records, ok := result.Config["records"].([]any)
|
|
if !ok || len(records) != 2 {
|
|
t.Fatalf("scheduled records = %#v", result.Config["records"])
|
|
}
|
|
firstRecord, ok := records[0].(map[string]any)
|
|
if !ok || firstRecord["id"] != "child" {
|
|
t.Fatalf("scheduled records = %#v", result.Config["records"])
|
|
}
|
|
}
|
|
sources := result.Sources()
|
|
check := sources[len(sources)-1].VersionCheck
|
|
if check == nil || check.Path.String() != "/records/0/version" || check.Location == nil || check.Location.SourceID != sources[len(sources)-1].ID {
|
|
t.Fatalf("source-local evidence changed: %#v", check)
|
|
}
|
|
if sensitivePath != "" {
|
|
if !check.Redacted || check.Version.Text != "" {
|
|
t.Fatal("shifted version evidence was disclosed")
|
|
}
|
|
} else if check.Redacted || check.Version.Text != "PRIVATE-VERSION-CANARY" {
|
|
t.Fatal("unclassified version evidence was lost")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFacadeSourceVersionOwnershipAcceptsContextualIndexes(t *testing.T) {
|
|
t.Parallel()
|
|
parsed, err := configkit.ParsePath("/records/0/version")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, grouped := range []bool{false, true} {
|
|
for _, path := range []configkit.Path{configkit.Keys("records").Index(0).Key("version"), parsed} {
|
|
definition := configkit.Definition[map[string]any]{
|
|
SequenceCompositions: []configkit.SequenceComposition{{Path: configkit.Keys("records"), IdentityField: "id"}},
|
|
Version: &configkit.VersionRule{Path: path, Check: func(configkit.Version, *configkit.Validation) error { return nil }},
|
|
}
|
|
first := "records: [{id: first, version: first}]\n"
|
|
child := "records: [{id: child, version: child}]\n"
|
|
var result *configkit.Result[map[string]any]
|
|
if grouped {
|
|
result, err = configkit.LoadLayered(configkit.Input{Source: configkit.Bytes("main", []byte(first))}, configkit.LayeredOptions[map[string]any]{
|
|
Definition: definition, SourceVersionMode: configkit.SourceVersionModeRequired,
|
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Bytes("lower", []byte(child))}}}},
|
|
})
|
|
} else {
|
|
filesystem := configkit.NewFilesystem("versions", fstest.MapFS{"main.yml": {Data: []byte("configkit: {imports: [{path: child.yml}]}\n" + first)}, "child.yml": {Data: []byte(child)}})
|
|
result, err = configkit.Load(filesystem.File("main.yml"), configkit.Options[map[string]any]{Definition: definition, SourceVersionMode: configkit.SourceVersionModeRequired, Imports: true})
|
|
}
|
|
if err != nil || result == nil {
|
|
t.Fatalf("grouped=%t path=%#v: %v", grouped, path, err)
|
|
}
|
|
want := 2
|
|
if grouped {
|
|
want = 1
|
|
}
|
|
if len(result.Config["records"].([]any)) != want {
|
|
t.Fatal("version path changed composition")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFacadeItemMainKeepsDefaultVersionOwnership(t *testing.T) {
|
|
t.Parallel()
|
|
for _, layered := range []bool{false, true} {
|
|
for _, protected := range []bool{true, false} {
|
|
body, helper := "id: main\n", "version: imported-version\n"
|
|
if !protected {
|
|
body += "version: main-version\n"
|
|
helper = "other: {version: permitted}\n"
|
|
}
|
|
filesystem := configkit.NewFilesystem("item-main", fstest.MapFS{
|
|
"main.yml": {Data: []byte(body + "configkit: {imports: [{path: helper.yml}]}\n")}, "helper.yml": {Data: []byte(helper)},
|
|
})
|
|
checks := 0
|
|
definition := configkit.Definition[map[string]any]{Version: &configkit.VersionRule{
|
|
Path: configkit.Keys("records").Index(0).Key("version"),
|
|
Check: func(version configkit.Version, _ *configkit.Validation) error {
|
|
checks++
|
|
if version.Text != "main-version" {
|
|
t.Error("helper supplied main version")
|
|
}
|
|
return nil
|
|
},
|
|
}}
|
|
var err error
|
|
if layered {
|
|
_, err = configkit.LoadLayered[map[string]any](configkit.Input{Source: filesystem.File("main.yml"), Imports: true, Destination: configkit.Keys("records"), PlacementMode: configkit.PlacementModeSequenceItem}, configkit.LayeredOptions[map[string]any]{Definition: definition})
|
|
} else {
|
|
_, err = configkit.Load(filesystem.File("main.yml"), configkit.Options[map[string]any]{Definition: definition, Imports: true, Destination: configkit.Keys("records"), PlacementMode: configkit.PlacementModeSequenceItem})
|
|
}
|
|
if protected {
|
|
loadError, ok := err.(*configkit.LoadError)
|
|
if !ok || len(loadError.Diagnostics()) != 1 || loadError.Diagnostics()[0].Code != configkit.CodeImportVersion || loadError.Diagnostics()[0].Path.String() != "/records/0/version" || checks != 0 {
|
|
t.Fatalf("layered=%t ownership error = %v, checks=%d", layered, err, checks)
|
|
}
|
|
} else if err != nil || checks != 1 {
|
|
t.Fatalf("unrelated helper version rejected: %v, checks=%d", err, checks)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type sourceVersionModeReader struct {
|
|
*strings.Reader
|
|
reads int
|
|
}
|
|
|
|
func TestFacadeSourceVersionEvidenceRedactsCollidingShapes(t *testing.T) {
|
|
t.Parallel()
|
|
type header struct {
|
|
Version string `yaml:"version"`
|
|
}
|
|
type secretHeader struct {
|
|
Version string `yaml:"version" configkit:"secret"`
|
|
}
|
|
type configuration struct {
|
|
Meta map[string]header `yaml:"meta"`
|
|
Data struct {
|
|
Meta map[string]secretHeader `yaml:"meta"`
|
|
} `yaml:"data"`
|
|
}
|
|
path, err := configkit.ParsePath("/meta/0/version")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, reverse := range []bool{false, true} {
|
|
groups := []configkit.Underlay{
|
|
{Inputs: []configkit.Input{{
|
|
Source: configkit.Bytes("mapping", []byte("meta: {\"0\": {version: PRIVATE-HEADER-CANARY}}\n")),
|
|
Destination: configkit.Keys("data"),
|
|
}}},
|
|
{Inputs: []configkit.Input{{
|
|
Source: configkit.Bytes("sequence", []byte("meta: [{version: another-header}]\n")),
|
|
Destination: configkit.Keys("data"),
|
|
}}},
|
|
}
|
|
if reverse {
|
|
groups[0], groups[1] = groups[1], groups[0]
|
|
}
|
|
loaded, err := configkit.LoadLayered(configkit.Input{
|
|
Source: configkit.Bytes("main", []byte("meta: {\"0\": {version: main}}\ndata: {meta: null}\n")),
|
|
}, configkit.LayeredOptions[configuration]{
|
|
Definition: configkit.Definition[configuration]{Version: &configkit.VersionRule{
|
|
Path: path, Check: func(configkit.Version, *configkit.Validation) error { return nil },
|
|
}},
|
|
SourceVersionMode: configkit.SourceVersionModeRequired, Underlays: groups,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if loaded.Config.Meta["0"].Version != "main" || loaded.Config.Data.Meta != nil {
|
|
t.Fatalf("reverse=%v: final payload changed: %#v", reverse, loaded.Config)
|
|
}
|
|
foundMapping := false
|
|
for _, source := range loaded.Sources() {
|
|
if source.Label != "mapping" {
|
|
continue
|
|
}
|
|
foundMapping = true
|
|
if check := source.VersionCheck; check == nil || !check.Version.Present || !check.Redacted || check.Version.Text != "" {
|
|
t.Errorf("reverse=%v: schema-tagged header was not redacted", reverse)
|
|
}
|
|
}
|
|
if !foundMapping {
|
|
t.Fatal("missing mapping source record")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFacadeSourceVersionModesPreserveImportOptIn(t *testing.T) {
|
|
t.Parallel()
|
|
for _, mode := range []configkit.SourceVersionMode{configkit.SourceVersionModeIfPresent, configkit.SourceVersionModeRequired} {
|
|
for _, underlay := range []bool{false, true} {
|
|
bad := configkit.Bytes("disabled-imports", []byte("version: accepted\nconfigkit: {imports: [{path: missing.yml}]}\nvalue: visible\n"))
|
|
definition := configkit.Definition[map[string]any]{Version: &configkit.VersionRule{
|
|
Path: configkit.Keys("version"), Check: func(configkit.Version, *configkit.Validation) error { return nil },
|
|
}}
|
|
var err error
|
|
var resultPresent bool
|
|
wantSource := "source-1"
|
|
if underlay {
|
|
loaded, loadErr := configkit.LoadLayered(configkit.Input{Source: configkit.Bytes("main", []byte("version: main\n"))}, configkit.LayeredOptions[map[string]any]{
|
|
Definition: definition, SourceVersionMode: mode,
|
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: bad}}}},
|
|
})
|
|
err, resultPresent, wantSource = loadErr, loaded != nil, "source-2"
|
|
} else {
|
|
loaded, loadErr := configkit.Load(bad, configkit.Options[map[string]any]{Definition: definition, SourceVersionMode: mode})
|
|
err, resultPresent = loadErr, loaded != nil
|
|
}
|
|
var failure *configkit.LoadError
|
|
if resultPresent || !errors.As(err, &failure) {
|
|
t.Errorf("mode=%v underlay=%v: import opt-in did not fail safely", mode, underlay)
|
|
continue
|
|
}
|
|
diagnostics := failure.Diagnostics()
|
|
if len(diagnostics) != 1 || diagnostics[0].Code != configkit.CodeImportDisabled || diagnostics[0].Path.String() != "/configkit" ||
|
|
diagnostics[0].Primary == nil || diagnostics[0].Primary.SourceID != wantSource {
|
|
t.Errorf("mode=%v underlay=%v: diagnostics=%#v", mode, underlay, diagnostics)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFacadeSourceVersionAbsenceKeepsCompleteEvidencePath(t *testing.T) {
|
|
t.Parallel()
|
|
for _, test := range []struct {
|
|
name, main, underlay string
|
|
path configkit.Path
|
|
}{
|
|
{"missing member", "meta: {version: main}\n", "{}\n", configkit.Keys("meta", "version")},
|
|
{"scalar obstruction", "meta: {version: main}\n", "meta: false\n", configkit.Keys("meta", "version")},
|
|
{"unavailable index", "meta: [null, null, null, {version: main}]\n", "meta: []\n", configkit.Keys("meta").Index(3).Key("version")},
|
|
} {
|
|
for _, mode := range []configkit.SourceVersionMode{configkit.SourceVersionModeIfPresent, configkit.SourceVersionModeRequired} {
|
|
var callbackEvidence *configkit.SourceVersionCheck
|
|
loaded, err := configkit.LoadLayered(configkit.Input{Source: configkit.Bytes("main", []byte(test.main))}, configkit.LayeredOptions[map[string]any]{
|
|
SourceVersionMode: mode,
|
|
Definition: configkit.Definition[map[string]any]{
|
|
Version: &configkit.VersionRule{Path: test.path, Check: func(configkit.Version, *configkit.Validation) error { return nil }},
|
|
Resolve: func(_ *map[string]any, resolution *configkit.Resolution) error {
|
|
source, found := resolution.Source("source-2")
|
|
if !found {
|
|
t.Fatal("missing callback source")
|
|
}
|
|
callbackEvidence = source.VersionCheck
|
|
return nil
|
|
},
|
|
},
|
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{
|
|
Source: configkit.Bytes("underlay", []byte(test.underlay)), Destination: configkit.Keys("under"),
|
|
}}}},
|
|
})
|
|
var evidence *configkit.SourceVersionCheck
|
|
if mode == configkit.SourceVersionModeRequired {
|
|
var failure *configkit.LoadError
|
|
if loaded != nil || !errors.As(err, &failure) {
|
|
t.Fatalf("%s: required omission did not fail", test.name)
|
|
}
|
|
diagnostics := failure.Diagnostics()
|
|
if len(diagnostics) != 1 || diagnostics[0].Code != configkit.CodeVersionError || diagnostics[0].Path.String() != test.path.String() {
|
|
t.Fatalf("%s: required diagnostics=%#v", test.name, diagnostics)
|
|
}
|
|
evidence = failure.Sources()[1].VersionCheck
|
|
} else {
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", test.name, err)
|
|
}
|
|
evidence = loaded.Sources()[1].VersionCheck
|
|
if callbackEvidence == nil || callbackEvidence.Path.String() != test.path.String() {
|
|
t.Errorf("%s: callback path is incomplete", test.name)
|
|
}
|
|
}
|
|
if evidence == nil || evidence.Version.Present || evidence.Path.String() != test.path.String() ||
|
|
evidence.Location == nil || evidence.Location.SourceID != "source-2" || evidence.Location.Line != 1 {
|
|
t.Errorf("%s mode=%v: absence evidence=%#v", test.name, mode, evidence)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFacadeSourceVersionMissingHeaderDoesNotOverlapControl(t *testing.T) {
|
|
t.Parallel()
|
|
for _, test := range []struct {
|
|
name string
|
|
mode configkit.SourceVersionMode
|
|
control string
|
|
wantCode configkit.Code
|
|
}{
|
|
{"optional omission", configkit.SourceVersionModeIfPresent, "base", ""},
|
|
{"required omission", configkit.SourceVersionModeRequired, "base", configkit.CodeVersionError},
|
|
{"invalid control", configkit.SourceVersionModeIfPresent, "true", configkit.CodeInheritanceInvalid},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
main := configkit.Bytes("main", []byte("{}\n"))
|
|
if test.mode == configkit.SourceVersionModeRequired {
|
|
main = configkit.NoSource()
|
|
}
|
|
checks := 0
|
|
path := configkit.Keys("entries", "child", "extends", "version")
|
|
loaded, err := configkit.LoadLayered(configkit.Input{Source: main, Destination: configkit.Keys("main")}, configkit.LayeredOptions[map[string]any]{
|
|
SourceVersionMode: test.mode,
|
|
Definition: configkit.Definition[map[string]any]{Version: &configkit.VersionRule{
|
|
Path: path, Check: func(version configkit.Version, _ *configkit.Validation) error {
|
|
checks++
|
|
if version.Present {
|
|
t.Error("checker received a nonexistent declaration")
|
|
}
|
|
return nil
|
|
},
|
|
}},
|
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{
|
|
Source: configkit.Bytes("underlay", []byte("entries:\n base: {value: good}\n child: {extends: "+test.control+"}\n")), Destination: configkit.Keys("under"),
|
|
}}}},
|
|
})
|
|
if checks != 1 {
|
|
t.Errorf("checker ran %d times, want absent main only", checks)
|
|
}
|
|
if test.wantCode != "" {
|
|
var failure *configkit.LoadError
|
|
if loaded != nil || !errors.As(err, &failure) {
|
|
t.Fatal("invalid input did not fail safely")
|
|
}
|
|
if diagnostics := failure.Diagnostics(); len(diagnostics) != 1 || diagnostics[0].Code != test.wantCode {
|
|
t.Fatalf("diagnostics=%#v", diagnostics)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
child := loaded.Config["under"].(map[string]any)["entries"].(map[string]any)["child"].(map[string]any)
|
|
if child["value"] != "good" {
|
|
t.Fatal("valid sibling inheritance was not applied")
|
|
}
|
|
check := loaded.Sources()[1].VersionCheck
|
|
if check == nil || check.Version.Present || check.Path.String() != path.String() {
|
|
t.Fatal("optional omission evidence was not retained")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func (reader *sourceVersionModeReader) Read(data []byte) (int, error) {
|
|
reader.reads++
|
|
return reader.Reader.Read(data)
|
|
}
|
|
|
|
func TestFacadeSourceVersionModesAcrossOrdinaryAndLayeredLoads(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var checked []configkit.Version
|
|
definition := configkit.Definition[map[string]any]{Version: &configkit.VersionRule{
|
|
Path: configkit.Keys("meta", "version"),
|
|
Check: func(version configkit.Version, _ *configkit.Validation) error {
|
|
checked = append(checked, version)
|
|
return nil
|
|
},
|
|
}}
|
|
ordinary, err := configkit.Load(configkit.Bytes("ordinary", []byte("meta: {version: ordinary-v}\n")), configkit.Options[map[string]any]{
|
|
Definition: definition, SourceVersionMode: configkit.SourceVersionModeIfPresent,
|
|
})
|
|
if err != nil || ordinary == nil {
|
|
t.Fatalf("ordinary enabled load = %#v, %v", ordinary, err)
|
|
}
|
|
if got := ordinary.Config["meta"].(map[string]any)["version"]; got != "ordinary-v" {
|
|
t.Fatalf("main declaration was not retained: %#v", ordinary.Config)
|
|
}
|
|
|
|
directory := t.TempDir()
|
|
mainPath := filepath.Join(directory, "main.yml")
|
|
if err := os.WriteFile(mainPath, []byte("meta: {version: main-v}\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
layered, err := configkit.LoadLayered(configkit.Input{Source: configkit.File(mainPath)}, configkit.LayeredOptions[map[string]any]{
|
|
Definition: definition, SourceVersionMode: configkit.SourceVersionModeRequired,
|
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{
|
|
Source: configkit.Mapping("underlay", map[string]any{
|
|
"meta": map[string]any{"version": "underlay-v"}, "fallback": "present",
|
|
}),
|
|
Destination: configkit.Keys("defaults"),
|
|
}}}},
|
|
})
|
|
if err != nil || layered == nil {
|
|
t.Fatalf("layered enabled load = %#v, %v", layered, err)
|
|
}
|
|
defaults, ok := layered.Config["defaults"].(map[string]any)
|
|
if !ok || defaults["fallback"] != "present" {
|
|
t.Fatalf("layered underlay payload = %#v", layered.Config["defaults"])
|
|
}
|
|
if meta, ok := defaults["meta"].(map[string]any); !ok || len(meta) != 0 {
|
|
t.Fatalf("accepted underlay declaration was not consumed: %#v", defaults["meta"])
|
|
}
|
|
if len(checked) != 3 {
|
|
t.Fatalf("checker calls across separate mode uses = %#v", checked)
|
|
}
|
|
for _, want := range []string{"ordinary-v", "main-v", "underlay-v"} {
|
|
found := false
|
|
for _, version := range checked {
|
|
found = found || version.Present && version.Text == want
|
|
}
|
|
if !found {
|
|
t.Errorf("checked versions %#v do not include %q", checked, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFacadeSourceVersionModeZeroPreservesDefaultBehavior(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
checks := 0
|
|
result, err := configkit.Load(configkit.Bytes("default", []byte("version: v1\n")), configkit.Options[map[string]any]{
|
|
Definition: configkit.Definition[map[string]any]{Version: &configkit.VersionRule{
|
|
Path: configkit.Keys("version"),
|
|
Check: func(version configkit.Version, _ *configkit.Validation) error {
|
|
checks++
|
|
if version != (configkit.Version{Present: true, Text: "v1"}) {
|
|
t.Errorf("default version = %#v", version)
|
|
}
|
|
return nil
|
|
},
|
|
}},
|
|
})
|
|
if err != nil || result == nil || result.Config["version"] != "v1" || checks != 1 {
|
|
t.Fatalf("zero-mode load = %#v, %v; checks = %d", result, err, checks)
|
|
}
|
|
}
|
|
|
|
func TestFacadeSourceVersionModeValidationPrecedesSourceReads(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
validDefinition := configkit.Definition[map[string]any]{Version: &configkit.VersionRule{
|
|
Path: configkit.Keys("version"), Check: func(configkit.Version, *configkit.Validation) error { return nil },
|
|
}}
|
|
for _, test := range []struct {
|
|
name string
|
|
mode configkit.SourceVersionMode
|
|
def configkit.Definition[map[string]any]
|
|
layered bool
|
|
}{
|
|
{name: "unknown ordinary mode", mode: configkit.SourceVersionMode(255), def: validDefinition},
|
|
{name: "ordinary mode requires checker", mode: configkit.SourceVersionModeIfPresent},
|
|
{name: "unknown layered mode", mode: configkit.SourceVersionMode(255), def: validDefinition, layered: true},
|
|
{name: "layered mode requires checker", mode: configkit.SourceVersionModeRequired, layered: true},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
reader := &sourceVersionModeReader{Reader: strings.NewReader("version: v1\n")}
|
|
source := configkit.Reader("version.yml", reader)
|
|
var err error
|
|
if test.layered {
|
|
_, err = configkit.LoadLayered(configkit.Input{Source: source}, configkit.LayeredOptions[map[string]any]{
|
|
Definition: test.def, SourceVersionMode: test.mode,
|
|
})
|
|
} else {
|
|
_, err = configkit.Load(source, configkit.Options[map[string]any]{
|
|
Definition: test.def, SourceVersionMode: test.mode,
|
|
})
|
|
}
|
|
var loadError *configkit.LoadError
|
|
if !errors.As(err, &loadError) || len(loadError.Diagnostics()) != 1 || loadError.Diagnostics()[0].Code != configkit.CodeOptionsInvalid {
|
|
t.Fatalf("invalid mode error = %#v", err)
|
|
}
|
|
if reader.reads != 0 {
|
|
t.Fatalf("invalid options read source %d times", reader.reads)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFacadeSourceVersionDefinitionCanBeReusedWithDifferentModes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
checks := 0
|
|
definition := configkit.Definition[map[string]any]{Version: &configkit.VersionRule{
|
|
Path: configkit.Keys("version"),
|
|
Check: func(version configkit.Version, _ *configkit.Validation) error {
|
|
checks++
|
|
if version.Present {
|
|
t.Errorf("missing declaration reached checker as %#v", version)
|
|
}
|
|
return nil
|
|
},
|
|
}}
|
|
source := configkit.Bytes("reused", []byte("value: retained\n"))
|
|
ifPresent, err := configkit.Load(source, configkit.Options[map[string]any]{
|
|
Definition: definition, SourceVersionMode: configkit.SourceVersionModeIfPresent,
|
|
})
|
|
if err != nil || ifPresent == nil || checks != 1 {
|
|
t.Fatalf("if-present load = %#v, %v; checks = %d", ifPresent, err, checks)
|
|
}
|
|
required, err := configkit.Load(source, configkit.Options[map[string]any]{
|
|
Definition: definition, SourceVersionMode: configkit.SourceVersionModeRequired,
|
|
})
|
|
var loadError *configkit.LoadError
|
|
if required != nil || !errors.As(err, &loadError) || len(loadError.Diagnostics()) != 1 || loadError.Diagnostics()[0].Code != configkit.CodeVersionError {
|
|
t.Fatalf("required load = %#v, %v", required, err)
|
|
}
|
|
if checks != 1 {
|
|
t.Fatalf("required absence reached checker: calls = %d", checks)
|
|
}
|
|
}
|
|
|
|
func TestFacadeSourceVersionEvidenceIsSanitizedAndDetached(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const canary = "PRIVATE-SOURCE-VERSION-CANARY"
|
|
directory := t.TempDir()
|
|
path := filepath.Join(directory, "main.yml")
|
|
if err := os.WriteFile(path, []byte("version: "+canary+"\nvalue: visible\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var callbackSource configkit.SourceInfo
|
|
var callbackContext configkit.ResourceContext
|
|
definition := configkit.Definition[map[string]any]{
|
|
Version: &configkit.VersionRule{Path: configkit.Keys("version"), Check: func(configkit.Version, *configkit.Validation) error { return nil }},
|
|
Secret: func(candidate configkit.Path) bool { return candidate.String() == "/service/version" },
|
|
Resolve: func(_ *map[string]any, resolution *configkit.Resolution) error {
|
|
var found bool
|
|
callbackSource, found = resolution.Source("source-1")
|
|
if !found {
|
|
t.Fatal("resolution callback could not query the acquired source")
|
|
}
|
|
if callbackSource.VersionCheck == nil || !callbackSource.VersionCheck.Redacted || callbackSource.VersionCheck.Version.Text != "" {
|
|
t.Fatalf("callback source did not conservatively redact evidence: %#v", callbackSource.VersionCheck)
|
|
}
|
|
callbackContext, found = resolution.SourceContext("source-1")
|
|
if !found || callbackContext.FS == nil || callbackContext.BaseDir != "." {
|
|
t.Fatalf("callback source context = %#v, found=%v", callbackContext, found)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
loaded, err := configkit.LoadLayered(configkit.Input{Source: configkit.File(path), Destination: configkit.Keys("service")}, configkit.LayeredOptions[map[string]any]{
|
|
Definition: definition, SourceVersionMode: configkit.SourceVersionModeIfPresent,
|
|
})
|
|
if err != nil || loaded == nil {
|
|
t.Fatalf("LoadLayered = %#v, %v", loaded, err)
|
|
}
|
|
if callbackSource.VersionCheck == nil || !callbackSource.VersionCheck.Redacted || callbackSource.VersionCheck.Version.Text != "" {
|
|
t.Fatalf("callback evidence lost its conservative snapshot: %#v", callbackSource.VersionCheck)
|
|
}
|
|
callbackSource.VersionCheck.Path = configkit.Keys("mutated-callback")
|
|
callbackSource.VersionCheck.Version.Text = canary
|
|
callbackSource.VersionCheck.Location.Line = 98
|
|
finalSources := loaded.Sources()
|
|
if len(finalSources) != 1 || finalSources[0].Path != path || finalSources[0].VersionCheck == nil {
|
|
t.Fatalf("final sources = %#v", finalSources)
|
|
}
|
|
check := finalSources[0].VersionCheck
|
|
if check.Path.String() != "/version" || check.Version != (configkit.Version{Present: true}) || !check.Redacted ||
|
|
check.Location == nil || check.Location.SourceID != finalSources[0].ID || check.Location.Line != 1 {
|
|
t.Fatalf("final classified evidence = %#v", check)
|
|
}
|
|
if err := os.Remove(path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if callbackContext.FS == nil || callbackContext.BaseDir != "." {
|
|
t.Fatalf("resource metadata changed after removing the source: %#v", callbackContext)
|
|
}
|
|
check.Path = configkit.Keys("mutated")
|
|
check.Location.Line = 99
|
|
check.Redacted = false
|
|
check.Version.Text = "mutated"
|
|
repeated := loaded.Sources()
|
|
if len(repeated) != 1 || repeated[0].VersionCheck.Path.String() != "/version" ||
|
|
repeated[0].VersionCheck.Location.Line != 1 || !repeated[0].VersionCheck.Redacted ||
|
|
repeated[0].VersionCheck.Version.Text != "" {
|
|
t.Fatalf("source evidence was aliased or re-read after source removal: %#v", repeated)
|
|
}
|
|
|
|
publicText := "PUBLIC-SOURCE-VERSION"
|
|
var publicCallback configkit.SourceInfo
|
|
public, err := configkit.Load(configkit.Bytes("public", []byte("version: "+publicText+"\n")), configkit.Options[map[string]any]{
|
|
SourceVersionMode: configkit.SourceVersionModeIfPresent,
|
|
Definition: configkit.Definition[map[string]any]{
|
|
Version: definition.Version,
|
|
Resolve: func(_ *map[string]any, resolution *configkit.Resolution) error {
|
|
publicCallback, _ = resolution.Source("source-1")
|
|
return nil
|
|
},
|
|
},
|
|
})
|
|
if err != nil || public == nil || len(public.Sources()) != 1 || public.Sources()[0].VersionCheck == nil {
|
|
t.Fatalf("unclassified source evidence = %#v, %v", public, err)
|
|
}
|
|
if got := public.Sources()[0].VersionCheck; got.Redacted || got.Version.Text != publicText || !got.Version.Present {
|
|
t.Fatalf("unclassified evidence = %#v", got)
|
|
}
|
|
if publicCallback.VersionCheck == nil || !publicCallback.VersionCheck.Redacted || publicCallback.VersionCheck.Version.Text != "" {
|
|
t.Fatalf("unclassified callback evidence was not initially conservative: %#v", publicCallback.VersionCheck)
|
|
}
|
|
}
|
|
|
|
func TestFacadeSourceVersionEvidenceOnFailedLoadsAndSyntheticSources(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
definition := configkit.Definition[map[string]any]{Version: &configkit.VersionRule{
|
|
Path: configkit.Keys("version"), Check: func(configkit.Version, *configkit.Validation) error { return nil },
|
|
}}
|
|
t.Run("early import acquisition failure", func(t *testing.T) {
|
|
const canary = "EARLY-ACQUISITION-SOURCE-VERSION-CANARY"
|
|
checked := 0
|
|
earlyDefinition := configkit.Definition[map[string]any]{Version: &configkit.VersionRule{
|
|
Path: configkit.Keys("version"), Check: func(configkit.Version, *configkit.Validation) error { checked++; return nil },
|
|
}}
|
|
path := filepath.Join(t.TempDir(), "main.yml")
|
|
contents := "version: " + canary + "\nconfigkit:\n imports:\n - path: missing.yml\n"
|
|
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err := configkit.LoadLayered(configkit.Input{Source: configkit.File(path), Imports: true}, configkit.LayeredOptions[map[string]any]{
|
|
Definition: earlyDefinition, SourceVersionMode: configkit.SourceVersionModeIfPresent,
|
|
})
|
|
var failure *configkit.LoadError
|
|
if !errors.As(err, &failure) || len(failure.Sources()) != 1 {
|
|
t.Fatalf("early acquisition failure sources = %#v, error %v", failure, err)
|
|
}
|
|
if checked != 1 {
|
|
t.Fatalf("main declaration checker calls = %d; error %v", checked, err)
|
|
}
|
|
check := failure.Sources()[0].VersionCheck
|
|
if check == nil || !check.Redacted || !check.Version.Present || check.Version.Text != "" {
|
|
t.Fatalf("early acquisition evidence was not conservatively redacted: %#v; sources %#v", check, failure.Sources())
|
|
}
|
|
if strings.Contains(err.Error(), canary) {
|
|
t.Fatalf("early acquisition failure disclosed source-version canary: %v", err)
|
|
}
|
|
})
|
|
_, err := configkit.Load(configkit.Bytes("malformed", []byte("version: true\n")), configkit.Options[map[string]any]{
|
|
Definition: definition, SourceVersionMode: configkit.SourceVersionModeIfPresent,
|
|
})
|
|
var loadError *configkit.LoadError
|
|
if !errors.As(err, &loadError) {
|
|
t.Fatalf("malformed version error = %v", err)
|
|
}
|
|
sources := loadError.Sources()
|
|
if len(sources) != 1 || sources[0].VersionCheck == nil || !sources[0].VersionCheck.Redacted ||
|
|
!sources[0].VersionCheck.Version.Present || sources[0].VersionCheck.Version.Text != "" || sources[0].VersionCheck.Path.String() != "/version" {
|
|
t.Fatalf("failed-load source evidence = %#v", sources)
|
|
}
|
|
sources[0].VersionCheck.Path = configkit.Keys("mutated-error")
|
|
sources[0].VersionCheck.Version.Text = "mutated"
|
|
sources[0].VersionCheck.Location.Line = 99
|
|
if again := loadError.Sources()[0].VersionCheck; again.Path.String() != "/version" || again.Version.Text != "" || again.Location.Line == 99 {
|
|
t.Fatalf("LoadError source evidence was aliased: %#v", again)
|
|
}
|
|
for _, test := range []struct {
|
|
name string
|
|
def configkit.Definition[map[string]any]
|
|
canary string
|
|
}{
|
|
{
|
|
name: "resolver failure",
|
|
canary: "RESOLVER-SOURCE-VERSION-CANARY",
|
|
def: configkit.Definition[map[string]any]{Version: definition.Version, Resolve: func(*map[string]any, *configkit.Resolution) error {
|
|
return errors.New("resolver failed")
|
|
}},
|
|
},
|
|
{
|
|
name: "validator failure",
|
|
canary: "VALIDATOR-SOURCE-VERSION-CANARY",
|
|
def: configkit.Definition[map[string]any]{Version: definition.Version, Validate: func(_ *map[string]any, validation *configkit.Validation) {
|
|
validation.Error(configkit.Keys("value"), "invalid value")
|
|
}},
|
|
},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
_, err := configkit.Load(configkit.Bytes("late-failure", []byte("version: "+test.canary+"\nvalue: configured\n")), configkit.Options[map[string]any]{
|
|
Definition: test.def, SourceVersionMode: configkit.SourceVersionModeIfPresent,
|
|
})
|
|
var failure *configkit.LoadError
|
|
if !errors.As(err, &failure) || len(failure.Sources()) != 1 {
|
|
t.Fatalf("late failure sources = %#v, error %v", failure, err)
|
|
}
|
|
if strings.Contains(err.Error(), test.canary) {
|
|
t.Fatalf("late failure disclosed source-version canary: %v", err)
|
|
}
|
|
check := failure.Sources()[0].VersionCheck
|
|
if check == nil || !check.Redacted || !check.Version.Present || check.Version.Text != "" {
|
|
t.Fatalf("late-failure evidence was not conservatively redacted: %#v", check)
|
|
}
|
|
})
|
|
}
|
|
|
|
defaultResult, err := configkit.Load(configkit.Bytes("default", []byte("version: 7\n")), configkit.Options[map[string]any]{})
|
|
if err != nil || defaultResult == nil || len(defaultResult.Sources()) != 1 || defaultResult.Sources()[0].VersionCheck != nil {
|
|
t.Fatalf("default source version check = %#v, %v", defaultResult, err)
|
|
}
|
|
|
|
withCLI, err := configkit.Load(configkit.Bytes("main", []byte("version: 8\nvalue: old\n")), configkit.Options[map[string]any]{
|
|
Definition: definition, SourceVersionMode: configkit.SourceVersionModeIfPresent,
|
|
Overrides: []configkit.Override{{Path: configkit.Keys("value"), Value: "new", Flag: "--value"}},
|
|
})
|
|
if err != nil || withCLI == nil {
|
|
t.Fatalf("enabled load with CLI source = %#v, %v", withCLI, err)
|
|
}
|
|
sawCLI := false
|
|
for _, source := range withCLI.Sources() {
|
|
if source.Kind == configkit.SourceKindCLI && source.VersionCheck != nil {
|
|
t.Fatalf("synthetic CLI source acquired declaration evidence: %#v", source)
|
|
}
|
|
sawCLI = sawCLI || source.Kind == configkit.SourceKindCLI
|
|
}
|
|
if !sawCLI {
|
|
t.Fatalf("CLI source record missing: %#v", withCLI.Sources())
|
|
}
|
|
}
|
|
|
|
func TestFacadeSourceVersionEvidencePreservesAbsenceAndNull(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var checked []configkit.Version
|
|
loaded, err := configkit.LoadLayered(configkit.Input{Source: configkit.Bytes("main", []byte("version: 17\n"))}, configkit.LayeredOptions[map[string]any]{
|
|
SourceVersionMode: configkit.SourceVersionModeIfPresent,
|
|
Definition: configkit.Definition[map[string]any]{Version: &configkit.VersionRule{
|
|
Path: configkit.Keys("version"),
|
|
Check: func(version configkit.Version, _ *configkit.Validation) error {
|
|
checked = append(checked, version)
|
|
return nil
|
|
},
|
|
}},
|
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{
|
|
{Source: configkit.Bytes("absent", []byte("value: visible\n")), Destination: configkit.Keys("absent")},
|
|
{Source: configkit.Bytes("null", []byte("version: null\nvalue: visible\n")), Destination: configkit.Keys("nullable")},
|
|
}}},
|
|
})
|
|
if err != nil || loaded == nil {
|
|
t.Fatalf("LoadLayered = %#v, %v", loaded, err)
|
|
}
|
|
if len(checked) != 2 || !checked[0].Present || checked[0].Text != "17" ||
|
|
!checked[1].Present || !checked[1].Null {
|
|
t.Fatalf("checker calls = %#v; absent contribution must skip while null is checked", checked)
|
|
}
|
|
sources := loaded.Sources()
|
|
if len(sources) != 3 || sources[1].VersionCheck == nil || sources[2].VersionCheck == nil {
|
|
t.Fatalf("source version evidence = %#v", sources)
|
|
}
|
|
absent := sources[1].VersionCheck
|
|
if absent.Version.Present || absent.Path.String() != "/version" || absent.Location == nil || absent.Redacted {
|
|
t.Fatalf("optional omission evidence = %#v", absent)
|
|
}
|
|
null := sources[2].VersionCheck
|
|
if !null.Version.Present || !null.Version.Null || null.Version.Text != "" || null.Path.String() != "/version" {
|
|
t.Fatalf("null declaration evidence = %#v", null)
|
|
}
|
|
}
|
|
|
|
func TestFacadeSourceVersionLifecycleKeepsConsumedHeadersOutOfPayloadHistory(t *testing.T) {
|
|
type serviceConfig struct {
|
|
Version uint64 `yaml:"version"`
|
|
Count int `yaml:"count"`
|
|
Enabled bool `yaml:"enabled"`
|
|
Nullable *string `yaml:"nullable"`
|
|
Revision string `yaml:"revision"`
|
|
Defaulted string `yaml:"defaulted"`
|
|
Resolved string `yaml:"resolved"`
|
|
Credential configkit.Secret `yaml:"credential"`
|
|
}
|
|
type configuration struct {
|
|
Service serviceConfig `yaml:"service"`
|
|
}
|
|
t.Setenv("CONFIGKIT_LIFECYCLE_SECRET", "lifecycle-secret-value")
|
|
var checked []configkit.Version
|
|
var validationCalls int
|
|
definition := configkit.Definition[configuration]{
|
|
Defaults: func() configuration {
|
|
return configuration{Service: serviceConfig{Defaulted: "from defaults"}}
|
|
},
|
|
Version: &configkit.VersionRule{
|
|
Path: configkit.Keys("version"),
|
|
Check: func(version configkit.Version, _ *configkit.Validation) error {
|
|
checked = append(checked, version)
|
|
return nil
|
|
},
|
|
},
|
|
Resolve: func(current *configuration, resolution *configkit.Resolution) error {
|
|
current.Service.Resolved = "resolved once"
|
|
resolution.Record(configkit.Keys("service", "resolved"), configkit.ChangeNormalization, "derive resolved value")
|
|
return nil
|
|
},
|
|
Validate: func(current *configuration, validation *configkit.Validation) {
|
|
validationCalls++
|
|
if current.Service.Version != 9007199254740993 || current.Service.Count != 0 || current.Service.Enabled ||
|
|
current.Service.Nullable != nil || current.Service.Revision != "ordinary-payload" || current.Service.Defaulted != "from defaults" ||
|
|
current.Service.Resolved != "resolved once" || current.Service.Credential.Value() != "lifecycle-secret-value" {
|
|
validation.Error(configkit.Keys("service"), "assembled values did not survive source declaration processing")
|
|
}
|
|
},
|
|
}
|
|
|
|
load := func(header string) *configkit.Result[configuration] {
|
|
t.Helper()
|
|
result, err := configkit.LoadLayered(configkit.Input{
|
|
Source: configkit.Bytes("main", []byte("version: 9007199254740993\n")), Destination: configkit.Keys("service"),
|
|
}, configkit.LayeredOptions[configuration]{
|
|
Definition: definition, SourceVersionMode: configkit.SourceVersionModeIfPresent,
|
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{
|
|
Source: configkit.Bytes("underlay", []byte("version: "+header+"\ncount: 0\nenabled: false\nnullable: null\nrevision: ordinary-payload\ncredential: CONFIGKIT_LIFECYCLE_SECRET\n")),
|
|
Destination: configkit.Keys("service"),
|
|
}}}},
|
|
})
|
|
if err != nil || result == nil {
|
|
var failure *configkit.LoadError
|
|
var diagnostics []configkit.Diagnostic
|
|
if errors.As(err, &failure) {
|
|
diagnostics = failure.Diagnostics()
|
|
}
|
|
t.Fatalf("LoadLayered(%q) = %#v, %v; diagnostics %#v", header, result, err, diagnostics)
|
|
}
|
|
return result
|
|
}
|
|
|
|
first := load("underlay-release-a")
|
|
second := load("underlay-release-b")
|
|
if len(checked) != 4 || checked[0].Text != "9007199254740993" || checked[1].Text != "underlay-release-a" ||
|
|
checked[2].Text != "9007199254740993" || checked[3].Text != "underlay-release-b" {
|
|
t.Fatalf("source declarations checked = %#v", checked)
|
|
}
|
|
if validationCalls != 2 || first.Config.Service != second.Config.Service {
|
|
t.Fatalf("final configs or validation count = %#v, %#v, %d", first.Config, second.Config, validationCalls)
|
|
}
|
|
if differences, err := configkit.Diff(first, second); err != nil || len(differences) != 0 {
|
|
t.Fatalf("different consumed declarations created effective differences: %#v, %v", differences, err)
|
|
}
|
|
var firstEffective, secondEffective bytes.Buffer
|
|
if err := first.WriteEffective(&firstEffective); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := second.WriteEffective(&secondEffective); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if firstEffective.String() != secondEffective.String() || strings.Contains(firstEffective.String(), "underlay-release") {
|
|
t.Fatalf("consumed declarations affected effective rendering:\n%s\n%s", firstEffective.String(), secondEffective.String())
|
|
}
|
|
sourcesA, sourcesB := first.Sources(), second.Sources()
|
|
if len(sourcesA) != 2 || len(sourcesB) != 2 || sourcesA[1].VersionCheck == nil || sourcesB[1].VersionCheck == nil ||
|
|
sourcesA[1].VersionCheck.Version.Text != "underlay-release-a" || sourcesB[1].VersionCheck.Version.Text != "underlay-release-b" {
|
|
t.Fatalf("sanitized evidence did not retain distinct accepted declarations: %#v / %#v", sourcesA, sourcesB)
|
|
}
|
|
versionExplanation, found := first.Explain(configkit.Keys("service", "version"))
|
|
if !found {
|
|
t.Fatal("retained main version missing from effective explanation")
|
|
}
|
|
underlayID := sourcesA[1].ID
|
|
for _, location := range versionExplanation.Origin.Locations {
|
|
if location.SourceID == underlayID {
|
|
t.Fatalf("consumed underlay declaration entered effective origin: %#v", versionExplanation.Origin)
|
|
}
|
|
}
|
|
for _, change := range versionExplanation.History {
|
|
for _, location := range change.Origin.Locations {
|
|
if location.SourceID == underlayID {
|
|
t.Fatalf("consumed underlay declaration entered effective history: %#v", versionExplanation.History)
|
|
}
|
|
}
|
|
}
|
|
}
|