1570 lines
64 KiB
Go
1570 lines
64 KiB
Go
package engine
|
|
|
|
import (
|
|
"errors"
|
|
"path/filepath"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|
)
|
|
|
|
func TestDefaultMainVersionWarningSurvivesLayeredLoading(t *testing.T) {
|
|
t.Parallel()
|
|
type config struct {
|
|
Version string `yaml:"version"`
|
|
Count int `yaml:"count"`
|
|
Items []map[string]any `yaml:"items"`
|
|
}
|
|
for _, test := range []struct {
|
|
name string
|
|
ordinary bool
|
|
body, lower string
|
|
failure bool
|
|
}{
|
|
{name: "ordinary", ordinary: true},
|
|
{name: "layered", lower: "{}"},
|
|
{name: "later structural failure", body: "configkit:\n sequences: [{destination: [items], key: id}]\nitems: []\n", lower: "items: null", failure: true},
|
|
{name: "later decoding failure", body: "count: invalid\n", lower: "{}", failure: true},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
calls := 0
|
|
definition := Definition[config]{Version: &VersionRule{
|
|
Path: Keys("version"),
|
|
Check: func(version Version, validation *Validation) error {
|
|
calls++
|
|
if version.Text != "current" {
|
|
t.Errorf("main checker received %q", version.Text)
|
|
}
|
|
validation.Warning(Keys("version"), "accepted main version")
|
|
return nil
|
|
},
|
|
}}
|
|
source := Bytes("main", []byte("version: current\n"+test.body))
|
|
var loaded *testResult[config]
|
|
var err error
|
|
if test.ordinary {
|
|
loaded, err = testLoad(source, Options[config]{Definition: definition})
|
|
} else {
|
|
loaded, err = testGroupedLoad(loadRequest[config]{
|
|
main: loadInput{source: source}, definition: definition,
|
|
underlays: []loadGroup{{inputs: []loadInput{{source: Bytes("lower", []byte(test.lower))}}}},
|
|
})
|
|
}
|
|
var diagnostics []Diagnostic
|
|
if test.failure {
|
|
var loadErr *LoadError
|
|
if !errors.As(err, &loadErr) || loaded != nil {
|
|
t.Fatalf("expected failed load: %#v, %v", loaded, err)
|
|
}
|
|
diagnostics = loadErr.Diagnostics()
|
|
if !hasErrors(diagnostics) {
|
|
t.Fatal("later failure was not reported")
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
diagnostics = loaded.Diagnostics()
|
|
}
|
|
warnings := 0
|
|
for _, diagnostic := range diagnostics {
|
|
if diagnostic.Code == CodeVersionWarning && diagnostic.Severity == SeverityWarning {
|
|
warnings++
|
|
}
|
|
}
|
|
if calls != 1 || warnings != 1 {
|
|
t.Fatalf("main checker calls = %d, retained warnings = %d; diagnostics = %#v", calls, warnings, diagnostics)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSourceVersionAbsentPathKeepsUnresolvedSegmentIntent(t *testing.T) {
|
|
t.Parallel()
|
|
for _, test := range []struct {
|
|
name string
|
|
body string
|
|
path string
|
|
want []pathSegment
|
|
}{
|
|
{"missing member", "{}\n", "/meta/version", []pathSegment{{kind: pathSegmentKey, key: "meta"}, {kind: pathSegmentParsed, key: "version"}}},
|
|
{"scalar obstruction", "meta: false\n", "/meta/version", []pathSegment{{kind: pathSegmentKey, key: "meta"}, {kind: pathSegmentParsed, key: "version"}}},
|
|
{"unavailable index", "meta: []\n", "/meta/3/version", []pathSegment{{kind: pathSegmentKey, key: "meta"}, {kind: pathSegmentIndex, index: 3}, {kind: pathSegmentParsed, key: "version"}}},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
path, err := ParsePath(test.path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
resolved := resolveSourceVersionPath(sourceVersionTestDocument(t, test.body), path)
|
|
if resolved.present || !reflect.DeepEqual(resolved.resolvedPath.segments, test.want) {
|
|
t.Fatalf("absent path = %#v, want %#v", resolved.resolvedPath, test.want)
|
|
}
|
|
if !reflect.DeepEqual(path.segments, mustParseSourceVersionPath(t, test.path).segments) {
|
|
t.Fatal("lookup mutated the requested path")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFragmentVersionClaimUsesAssignedSequenceProjection(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fragment := sourceVersionTestDocument(t, "items: [{version: v1}]\n")
|
|
rangeForPeer := []sequenceRange{{sourceIndex: 1, path: Keys("items"), start: 1, end: 2}}
|
|
if node := fragmentVersionClaimProjected(fragment, &VersionRule{Path: Keys("items").Index(0).Key("version")}, 1, rangeForPeer); node != nil {
|
|
t.Fatalf("peer append claimed main index 0: %#v", node)
|
|
}
|
|
if node := fragmentVersionClaimProjected(fragment, &VersionRule{Path: Keys("items").Index(1).Key("version")}, 1, rangeForPeer); node == nil || node.Text != "v1" {
|
|
t.Fatalf("assigned peer index 1 claim = %#v, want its version node", node)
|
|
}
|
|
mappedRange := []sequenceRange{{
|
|
sourceIndex: 1, path: Keys("items"), start: 0, end: 3,
|
|
items: []sequenceItemProjection{{inputIndex: 0, outputIndex: 2}},
|
|
}}
|
|
if node := fragmentVersionClaimProjected(fragment, &VersionRule{Path: Keys("items").Index(0).Key("version")}, 1, mappedRange); node != nil {
|
|
t.Fatalf("unassigned mapped peer index claimed a version: %#v", node)
|
|
}
|
|
if node := fragmentVersionClaimProjected(fragment, &VersionRule{Path: Keys("items").Index(2).Key("version")}, 1, mappedRange); node == nil || node.Text != "v1" {
|
|
t.Fatalf("mapped peer index 2 claim = %#v, want its version node", node)
|
|
}
|
|
nested := sourceVersionTestDocument(t, "items: [{nested: [{version: nested-v}]}]\n")
|
|
nestedRanges := []sequenceRange{
|
|
{sourceIndex: 1, path: Keys("items"), start: 1, end: 2},
|
|
{sourceIndex: 1, path: Keys("items").Index(0).Key("nested"), start: 2, end: 3},
|
|
}
|
|
if node := fragmentVersionClaimProjected(nested, &VersionRule{Path: Keys("items").Index(1).Key("nested").Index(2).Key("version")}, 1, nestedRanges); node == nil || node.Text != "nested-v" {
|
|
t.Fatalf("nested assigned claim = %#v, want nested version", node)
|
|
}
|
|
ancestor := sourceVersionTestDocument(t, "items: {version: replacement}\n")
|
|
if node := fragmentVersionClaim(ancestor, &VersionRule{Path: Keys("items").Index(0).Key("version")}); node == nil || node.Kind != document.Mapping {
|
|
t.Fatalf("destructive sequence ancestor claim = %#v, want mapping node", node)
|
|
}
|
|
|
|
effective := effectiveSourceVersionRuleForPlacement(&VersionRule{Path: Keys("version")}, Keys("catalog", "entries"), sourceVersionModeRequired, placementModeSequenceItem)
|
|
if got, want := effective.Path.String(), "/catalog/entries/0/version"; got != want {
|
|
t.Fatalf("item effective version path = %q, want %q", got, want)
|
|
}
|
|
defaultRule := effectiveSourceVersionRuleForPlacement(&VersionRule{Path: Keys("version")}, Keys("catalog", "entries"), sourceVersionModeDefault, placementModeSequenceItem)
|
|
if got, want := defaultRule.Path.String(), "/version"; got != want {
|
|
t.Fatalf("default effective version path = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
type sourceVersionMountedService struct {
|
|
Version string `yaml:"version"`
|
|
}
|
|
|
|
type sourceVersionMountedConfig struct {
|
|
Service sourceVersionMountedService `yaml:"service"`
|
|
}
|
|
|
|
type sourceVersionNullConfig struct {
|
|
Version *string `yaml:"version"`
|
|
}
|
|
|
|
type sourceVersionImportGroup struct {
|
|
Name string `yaml:"name"`
|
|
}
|
|
|
|
type sourceVersionImportConfig struct {
|
|
Service struct {
|
|
Version string `yaml:"version"`
|
|
Groups map[string]sourceVersionImportGroup `yaml:"groups"`
|
|
} `yaml:"service"`
|
|
}
|
|
|
|
func sourceVersionTestDocument(t *testing.T, input string) *document.Node {
|
|
t.Helper()
|
|
parsed, diagnostics := parseSource(Bytes("source-version.yml", []byte(input)), "source-version", defaultParseLimits())
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("parse diagnostics = %#v", diagnostics)
|
|
}
|
|
return parsed.document
|
|
}
|
|
|
|
func requireSourceVersionDiagnostic(t *testing.T, diagnostics []Diagnostic, code Code, path string) Diagnostic {
|
|
t.Helper()
|
|
for _, diagnostic := range diagnostics {
|
|
if diagnostic.Code == code && diagnostic.Path.String() == path {
|
|
return diagnostic
|
|
}
|
|
}
|
|
t.Fatalf("diagnostics = %#v, want %s at %q", diagnostics, code, path)
|
|
return Diagnostic{}
|
|
}
|
|
|
|
func TestSourceVersionExtractionPreservesVersionVocabulary(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want Version
|
|
}{
|
|
{name: "string", input: "version: \"app.v7\"\n", want: Version{Present: true, Text: "app.v7"}},
|
|
{name: "integer", input: "version: 123456789012345678901234567890\n", want: Version{Present: true, Text: "123456789012345678901234567890"}},
|
|
{name: "null", input: "version: null\n", want: Version{Present: true, Null: true}},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
root := sourceVersionTestDocument(t, test.input)
|
|
var checked Version
|
|
rule := &VersionRule{Path: Keys("version"), Check: func(version Version, _ *Validation) error {
|
|
checked = version
|
|
return nil
|
|
}}
|
|
result := processSourceVersion(root, rule, sourceVersionContribution, Keys("collections", "weather"), sourceVersionModeIfPresent)
|
|
if len(result.diagnostics) != 0 || !result.evidence.checked || result.evidence.version != test.want || checked != test.want {
|
|
t.Fatalf("result = %#v, checked = %#v", result, checked)
|
|
}
|
|
if _, found := documentAtPath(result.payload, rule.Path); found {
|
|
t.Fatal("accepted non-main version remained in the payload")
|
|
}
|
|
if node, found := documentAtPath(root, rule.Path); !found || node == nil {
|
|
t.Fatal("source document was mutated while consuming its version")
|
|
}
|
|
if result.evidence.destination.String() != "/collections/weather" {
|
|
t.Fatalf("evidence destination = %q", result.evidence.destination.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSourceVersionExtractionHandlesMissingAndMalformedValues(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
path Path
|
|
mode sourceVersionMode
|
|
role sourceVersionRole
|
|
wantCode Code
|
|
wantPresent bool
|
|
wantCallback bool
|
|
wantAbsence sourceVersionPathAbsence
|
|
}{
|
|
{
|
|
name: "missing mapping member is optional absence", input: "settings: {}\n",
|
|
path: Keys("settings", "version"), mode: sourceVersionModeIfPresent,
|
|
role: sourceVersionContribution, wantPresent: false, wantAbsence: sourceVersionMemberMissing,
|
|
},
|
|
{
|
|
name: "missing below scalar is optional absence", input: "settings: scalar\n",
|
|
path: Keys("settings", "version"), mode: sourceVersionModeIfPresent,
|
|
role: sourceVersionContribution, wantPresent: false, wantAbsence: sourceVersionPathObstructed,
|
|
},
|
|
{
|
|
name: "required absence reports nearest location", input: "settings: scalar\n",
|
|
path: Keys("settings", "version"), mode: sourceVersionModeRequired,
|
|
role: sourceVersionContribution, wantCode: CodeVersionError, wantAbsence: sourceVersionPathObstructed,
|
|
},
|
|
{
|
|
name: "unavailable sequence index is absence", input: "versions: [1]\n",
|
|
path: Keys("versions").Index(3), mode: sourceVersionModeRequired,
|
|
role: sourceVersionContribution, wantCode: CodeVersionError, wantAbsence: sourceVersionIndexUnavailable,
|
|
},
|
|
{
|
|
name: "required actual main absence", input: "name: demo\n", path: Keys("version"),
|
|
mode: sourceVersionModeRequired, role: sourceVersionMain, wantCode: CodeVersionError, wantAbsence: sourceVersionMemberMissing,
|
|
},
|
|
{
|
|
name: "main optional absence invokes checker", input: "name: demo\n", path: Keys("version"),
|
|
mode: sourceVersionModeIfPresent, role: sourceVersionMain, wantCallback: true, wantAbsence: sourceVersionMemberMissing,
|
|
},
|
|
{name: "boolean", input: "version: true\n", path: Keys("version"), mode: sourceVersionModeIfPresent, role: sourceVersionContribution, wantCode: CodeVersionType, wantPresent: true},
|
|
{name: "fraction", input: "version: 4.5\n", path: Keys("version"), mode: sourceVersionModeIfPresent, role: sourceVersionContribution, wantCode: CodeVersionType, wantPresent: true},
|
|
{name: "mapping", input: "version: {}\n", path: Keys("version"), mode: sourceVersionModeIfPresent, role: sourceVersionContribution, wantCode: CodeVersionType, wantPresent: true},
|
|
{name: "sequence", input: "version: []\n", path: Keys("version"), mode: sourceVersionModeIfPresent, role: sourceVersionContribution, wantCode: CodeVersionType, wantPresent: true},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
root := sourceVersionTestDocument(t, test.input)
|
|
called := false
|
|
rule := &VersionRule{Path: test.path, Check: func(Version, *Validation) error {
|
|
called = true
|
|
return nil
|
|
}}
|
|
result := processSourceVersion(root, rule, test.role, Path{}, test.mode)
|
|
if called != test.wantCallback {
|
|
t.Fatalf("checker called = %v, want %v", called, test.wantCallback)
|
|
}
|
|
if test.wantCode != "" {
|
|
diagnostic := requireSourceVersionDiagnostic(t, result.diagnostics, test.wantCode, test.path.String())
|
|
if result.payload != nil {
|
|
t.Fatalf("failed processing returned payload %#v", result.payload)
|
|
}
|
|
if diagnostic.Primary == nil || diagnostic.Primary.SourceID != "source-version" || diagnostic.Primary.Line != 1 {
|
|
t.Fatalf("diagnostic location = %#v", diagnostic.Primary)
|
|
}
|
|
} else if len(result.diagnostics) != 0 {
|
|
t.Fatalf("diagnostics = %#v", result.diagnostics)
|
|
}
|
|
if result.evidence == nil || result.evidence.version.Present != test.wantPresent {
|
|
t.Fatalf("evidence = %#v", result.evidence)
|
|
}
|
|
if result.evidence.absence != test.wantAbsence {
|
|
t.Fatalf("absence cause = %v, want %v", result.evidence.absence, test.wantAbsence)
|
|
}
|
|
if test.wantCode == CodeVersionType && (!result.evidence.redacted || result.evidence.version.Text != "") {
|
|
t.Fatalf("malformed value was retained: %#v", result.evidence)
|
|
}
|
|
if !test.wantCallback && result.evidence.checked {
|
|
t.Fatalf("evidence incorrectly records a callback: %#v", result.evidence)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSourceVersionExtractionConsumesOnlySelectedNodes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
destination := Keys("collections", "weather")
|
|
root := sourceVersionTestDocument(t, "settings:\n version: 8\n")
|
|
originalSettings, _ := documentAtPath(root, Keys("settings"))
|
|
result := processSourceVersion(root, &VersionRule{
|
|
Path: Keys("settings", "version"), Check: func(Version, *Validation) error { return nil },
|
|
}, sourceVersionContribution, destination, sourceVersionModeIfPresent)
|
|
settings, found := documentAtPath(result.payload, Keys("settings"))
|
|
if len(result.diagnostics) != 0 || !found || settings.Kind != document.Mapping || len(settings.Mapping) != 0 {
|
|
t.Fatalf("payload = %#v, diagnostics = %#v", result.payload, result.diagnostics)
|
|
}
|
|
if settings.Location != originalSettings.Location {
|
|
t.Fatalf("authored ancestor location changed: got %#v, want %#v", settings.Location, originalSettings.Location)
|
|
}
|
|
if original, found := documentAtPath(root, Keys("settings", "version")); !found || original.Text != "8" {
|
|
t.Fatalf("original source changed: %#v, found %v", original, found)
|
|
}
|
|
if len(result.evidence.ancestors) != 3 || result.evidence.ancestors[1].kind != document.Mapping {
|
|
t.Fatalf("ancestor shape = %#v", result.evidence.ancestors)
|
|
}
|
|
|
|
sequenceRoot := sourceVersionTestDocument(t, "versions: [5, 7, 9]\n")
|
|
originalVersions, _ := documentAtPath(sequenceRoot, Keys("versions"))
|
|
originalLastLocation := originalVersions.Sequence[2].Location
|
|
sequencePath, err := ParsePath("/versions/1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sequenceResult := processSourceVersion(sequenceRoot, &VersionRule{
|
|
Path: sequencePath, Check: func(version Version, _ *Validation) error {
|
|
if version.Text != "7" {
|
|
t.Fatalf("checked version = %#v", version)
|
|
}
|
|
return nil
|
|
},
|
|
}, sourceVersionContribution, Path{}, sourceVersionModeRequired)
|
|
versions, found := documentAtPath(sequenceResult.payload, Keys("versions"))
|
|
if len(sequenceResult.diagnostics) != 0 || !found || versions.Kind != document.Sequence || len(versions.Sequence) != 2 || versions.Sequence[0].Text != "5" || versions.Sequence[1].Text != "9" {
|
|
t.Fatalf("sequence payload = %#v, diagnostics = %#v", sequenceResult.payload, sequenceResult.diagnostics)
|
|
}
|
|
if versions.Sequence[1].Location != originalLastLocation {
|
|
t.Fatalf("remaining sequence location changed: got %#v, want %#v", versions.Sequence[1].Location, originalLastLocation)
|
|
}
|
|
if len(sequenceResult.evidence.resolvedPath.segments) != 2 || sequenceResult.evidence.resolvedPath.segments[1].kind != pathSegmentIndex || sequenceResult.evidence.resolvedPath.segments[1].index != 1 {
|
|
t.Fatalf("original sequence path = %#v", sequenceResult.evidence.resolvedPath)
|
|
}
|
|
|
|
itemRoot := sourceVersionTestDocument(t, "items:\n - name: first\n - version: 12\n")
|
|
itemResult := processSourceVersion(itemRoot, &VersionRule{
|
|
Path: Keys("items").Index(1).Key("version"), Check: func(Version, *Validation) error { return nil },
|
|
}, sourceVersionContribution, Path{}, sourceVersionModeIfPresent)
|
|
items, found := documentAtPath(itemResult.payload, Keys("items"))
|
|
if len(itemResult.diagnostics) != 0 || !found || len(items.Sequence) != 2 || items.Sequence[1].Kind != document.Mapping || len(items.Sequence[1].Mapping) != 0 {
|
|
t.Fatalf("sequence mapping payload = %#v, diagnostics = %#v", itemResult.payload, itemResult.diagnostics)
|
|
}
|
|
|
|
emptySequenceRoot := sourceVersionTestDocument(t, "versions: [7]\n")
|
|
emptySequenceResult := processSourceVersion(emptySequenceRoot, &VersionRule{
|
|
Path: Keys("versions").Index(0), Check: func(Version, *Validation) error { return nil },
|
|
}, sourceVersionContribution, Path{}, sourceVersionModeIfPresent)
|
|
emptySequence, found := documentAtPath(emptySequenceResult.payload, Keys("versions"))
|
|
if len(emptySequenceResult.diagnostics) != 0 || !found || emptySequence.Kind != document.Sequence || len(emptySequence.Sequence) != 0 {
|
|
t.Fatalf("empty sequence payload = %#v, diagnostics = %#v", emptySequenceResult.payload, emptySequenceResult.diagnostics)
|
|
}
|
|
}
|
|
|
|
func TestSourceVersionExtractionResolvesContextualPathSegments(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
path Path
|
|
wantRemainingPath bool
|
|
wantRemainingText string
|
|
}{
|
|
{name: "literal numeric mapping key", input: "versions:\n \"0\": 3\n other: 4\n", path: Keys("versions", "0")},
|
|
{name: "parsed numeric mapping key", input: "versions:\n \"0\": 3\n other: 4\n", path: mustParseSourceVersionPath(t, "/versions/0")},
|
|
{name: "parsed sequence index", input: "versions: [3, 4]\n", path: mustParseSourceVersionPath(t, "/versions/0"), wantRemainingPath: true, wantRemainingText: "4"},
|
|
{name: "explicit sequence index", input: "versions: [3, 4]\n", path: Keys("versions").Index(0), wantRemainingPath: true, wantRemainingText: "4"},
|
|
{name: "escaped mapping key", input: "a/b:\n version: 3\n", path: mustParseSourceVersionPath(t, "/a~1b/version")},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
root := sourceVersionTestDocument(t, test.input)
|
|
result := processSourceVersion(root, &VersionRule{
|
|
Path: test.path, Check: func(version Version, _ *Validation) error {
|
|
if version.Text != "3" {
|
|
t.Errorf("checked version = %#v", version)
|
|
}
|
|
return nil
|
|
},
|
|
}, sourceVersionContribution, Path{}, sourceVersionModeIfPresent)
|
|
if len(result.diagnostics) != 0 {
|
|
t.Fatalf("diagnostics = %#v", result.diagnostics)
|
|
}
|
|
remaining, found := documentAtPath(result.payload, test.path)
|
|
if found != test.wantRemainingPath || found && remaining.Text != test.wantRemainingText {
|
|
t.Fatalf("remaining selected path = %#v, found = %v", remaining, found)
|
|
}
|
|
if !result.evidence.version.Present || result.evidence.version.Text != "3" {
|
|
t.Fatalf("evidence = %#v", result.evidence)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSourceVersionExtractionRejectsStructuralOverlap(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
path Path
|
|
wantCode Code
|
|
}{
|
|
{
|
|
name: "inside import declaration", input: "configkit:\n imports:\n - path: child.yml\n",
|
|
path: Keys("configkit", "imports").Index(0).Key("path"), wantCode: CodeVersionError,
|
|
},
|
|
{
|
|
name: "extends control", input: "settings:\n extends: parent\n",
|
|
path: Keys("settings", "extends"), wantCode: CodeVersionError,
|
|
},
|
|
{
|
|
name: "invalid extends subtree", input: "settings:\n extends:\n version: 1\n",
|
|
path: Keys("settings", "extends", "version"), wantCode: CodeVersionError,
|
|
},
|
|
{
|
|
name: "malformed directive cannot be consumed", input: "configkit:\n imports: true\n",
|
|
path: Keys("configkit", "imports"), wantCode: CodeImportInvalid,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
root := sourceVersionTestDocument(t, test.input)
|
|
called := false
|
|
result := processSourceVersion(root, &VersionRule{Path: test.path, Check: func(Version, *Validation) error {
|
|
called = true
|
|
return nil
|
|
}}, sourceVersionContribution, Path{}, sourceVersionModeIfPresent)
|
|
diagnostic := requireSourceVersionDiagnostic(t, result.diagnostics, test.wantCode, test.path.String())
|
|
if called || result.payload != nil {
|
|
t.Fatalf("invalid overlap produced payload %#v or called checker %v", result.payload, called)
|
|
}
|
|
if test.wantCode == CodeVersionError && (result.evidence == nil || !result.evidence.redacted || result.evidence.version.Text != "") {
|
|
t.Fatalf("overlap evidence was not conservatively redacted: %#v", result.evidence)
|
|
}
|
|
if diagnostic.Primary == nil || diagnostic.Primary.SourceID != "source-version" {
|
|
t.Fatalf("diagnostic location = %#v", diagnostic.Primary)
|
|
}
|
|
})
|
|
}
|
|
|
|
unrelatedRoot := sourceVersionTestDocument(t, "configkit:\n imports:\n - path: child.yml\nmetadata:\n version: 4\n")
|
|
unrelatedResult := processSourceVersion(unrelatedRoot, &VersionRule{
|
|
Path: Keys("metadata", "version"), Check: func(Version, *Validation) error { return nil },
|
|
}, sourceVersionContribution, Path{}, sourceVersionModeIfPresent)
|
|
metadata, found := documentAtPath(unrelatedResult.payload, Keys("metadata"))
|
|
if len(unrelatedResult.diagnostics) != 0 || !found || len(metadata.Mapping) != 0 || len(unrelatedResult.imports) != 1 {
|
|
t.Fatalf("unrelated directive result = %#v", unrelatedResult)
|
|
}
|
|
|
|
ordinaryRoot := sourceVersionTestDocument(t, "extends_version: 4\n")
|
|
ordinaryResult := processSourceVersion(ordinaryRoot, &VersionRule{
|
|
Path: Keys("extends_version"), Check: func(Version, *Validation) error { return nil },
|
|
}, sourceVersionContribution, Path{}, sourceVersionModeIfPresent)
|
|
if len(ordinaryResult.diagnostics) != 0 || ordinaryResult.payload == nil {
|
|
t.Fatalf("ordinary extends_version key was rejected: %#v", ordinaryResult)
|
|
}
|
|
}
|
|
|
|
func TestSourceVersionDefaultModeDoesNotCheckOrConsume(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
root := sourceVersionTestDocument(t, "version: 2\n")
|
|
called := false
|
|
result := processSourceVersion(root, &VersionRule{Path: Keys("version"), Check: func(Version, *Validation) error {
|
|
called = true
|
|
return nil
|
|
}}, sourceVersionContribution, Path{}, sourceVersionModeDefault)
|
|
if len(result.diagnostics) != 0 || called || result.evidence != nil {
|
|
t.Fatalf("default result = %#v, checker called = %v", result, called)
|
|
}
|
|
if node, found := documentAtPath(result.payload, Keys("version")); !found || node.Text != "2" {
|
|
t.Fatalf("default payload = %#v", result.payload)
|
|
}
|
|
}
|
|
|
|
func TestSourceVersionNoSourceMainRetainsAbsentCallback(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
mode sourceVersionMode
|
|
}{
|
|
{name: "if present", mode: sourceVersionModeIfPresent},
|
|
{name: "required", mode: sourceVersionModeRequired},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
called := false
|
|
result := processSourceVersion(nil, &VersionRule{
|
|
Path: Keys("version"),
|
|
Check: func(version Version, _ *Validation) error {
|
|
called = true
|
|
if version != (Version{}) {
|
|
t.Errorf("NoSource version = %#v", version)
|
|
}
|
|
return nil
|
|
},
|
|
}, sourceVersionMain, Path{}, test.mode)
|
|
if !called || len(result.diagnostics) != 0 || result.evidence != nil || result.payload != nil {
|
|
t.Fatalf("NoSource result = %#v, callback called = %v", result, called)
|
|
}
|
|
|
|
type config struct{}
|
|
for _, reject := range []bool{false, true} {
|
|
called = false
|
|
definition := Definition[config]{Version: &VersionRule{
|
|
Path: Keys("version"),
|
|
Check: func(version Version, _ *Validation) error {
|
|
called = true
|
|
if version != (Version{}) {
|
|
t.Errorf("NoSource load version = %#v", version)
|
|
}
|
|
if reject {
|
|
return errors.New("rejected absent main")
|
|
}
|
|
return nil
|
|
},
|
|
}}
|
|
loaded, loadErr := testLoad(NoSource(), Options[config]{
|
|
Definition: definition, Imports: true, SourceVersionMode: test.mode,
|
|
})
|
|
if !called {
|
|
t.Fatal("NoSource load did not call the source-version checker")
|
|
}
|
|
if reject {
|
|
var failure *LoadError
|
|
if !errors.As(loadErr, &failure) {
|
|
t.Fatalf("rejected NoSource load = %v, want *LoadError", loadErr)
|
|
}
|
|
requireSourceVersionDiagnostic(t, failure.Diagnostics(), CodeVersionHookFailed, "/version")
|
|
continue
|
|
}
|
|
if loadErr != nil || loaded == nil || len(loaded.Sources()) != 0 {
|
|
t.Fatalf("accepted NoSource load = %#v, %v", loaded, loadErr)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSourceVersionLoadChecksMountedMainLocallyAndProtectsEffectivePath(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
checks := 0
|
|
definition := Definition[sourceVersionMountedConfig]{Version: &VersionRule{
|
|
Path: Keys("version"),
|
|
Check: func(version Version, validation *Validation) error {
|
|
checks++
|
|
if !version.Present || version.Null || version.Text != "9007199254740993" {
|
|
return errors.New("unexpected source-local version")
|
|
}
|
|
validation.Warning(Path{}, "advisory")
|
|
return nil
|
|
},
|
|
}}
|
|
main := Input{Source: Bytes("main.yml", []byte("version: 9007199254740993\n")), Destination: Keys("service")}
|
|
loaded, err := LoadLayered(main, testSecretBinding(), LayeredOptions[sourceVersionMountedConfig]{
|
|
Definition: definition, SourceVersionMode: sourceVersionModeRequired,
|
|
})
|
|
if err != nil || loaded == nil {
|
|
t.Fatalf("mounted main load = %#v, %v", loaded, err)
|
|
}
|
|
if loaded.Config.Service.Version != "9007199254740993" || checks != 1 {
|
|
t.Fatalf("mounted main config/callbacks = %#v, %d", loaded.Config, checks)
|
|
}
|
|
warnings := testLoaded(loaded).Diagnostics()
|
|
if len(warnings) != 1 || warnings[0].Code != CodeVersionWarning || warnings[0].Severity != SeverityWarning {
|
|
t.Fatalf("mounted main warnings = %#v", warnings)
|
|
}
|
|
|
|
checks = 0
|
|
_, err = LoadLayered(main, testSecretBinding(), LayeredOptions[sourceVersionMountedConfig]{
|
|
Definition: definition, SourceVersionMode: sourceVersionModeRequired,
|
|
Overrides: []Override{{Path: Keys("service").Key("version"), Value: "4", Flag: "--version"}},
|
|
})
|
|
var loadErr *LoadError
|
|
if !errors.As(err, &loadErr) {
|
|
t.Fatalf("mounted protected override error = %T %v, want *LoadError", err, err)
|
|
}
|
|
requireSourceVersionDiagnostic(t, loadErr.Diagnostics(), CodeOverrideVersion, "/service/version")
|
|
if checks != 1 {
|
|
t.Fatalf("mounted protected override callbacks = %d, want 1", checks)
|
|
}
|
|
}
|
|
|
|
func TestSourceVersionItemPlacementKeepsLocalChecksAndProtectsProjectedPath(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
load func(Definition[map[string]any]) error
|
|
}{
|
|
{
|
|
name: "ordinary",
|
|
load: func(definition Definition[map[string]any]) error {
|
|
_, err := Load(Mapping("main", map[string]any{"version": "v1", "name": "ordinary"}), testSecretBinding(), Options[map[string]any]{
|
|
Definition: definition, SourceVersionMode: sourceVersionModeRequired,
|
|
destination: Keys("catalog", "entries"), placementMode: placementModeSequenceItem,
|
|
Overrides: []Override{{Path: Keys("catalog", "entries"), Value: []any{}, Flag: "--entries"}},
|
|
})
|
|
return err
|
|
},
|
|
},
|
|
{
|
|
name: "layered",
|
|
load: func(definition Definition[map[string]any]) error {
|
|
_, err := LoadLayered(Input{Source: Mapping("main", map[string]any{"version": "v1", "name": "layered"}), Destination: Keys("catalog", "entries"), placementMode: placementModeSequenceItem}, testSecretBinding(), LayeredOptions[map[string]any]{
|
|
Definition: definition, SourceVersionMode: sourceVersionModeRequired,
|
|
Overrides: []Override{{Path: Keys("catalog", "entries"), Value: []any{}, Flag: "--entries"}},
|
|
})
|
|
return err
|
|
},
|
|
},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
checks := 0
|
|
definition := Definition[map[string]any]{Version: &VersionRule{
|
|
Path: Keys("version"),
|
|
Check: func(version Version, _ *Validation) error {
|
|
checks++
|
|
if !version.Present || version.Text != "v1" {
|
|
t.Errorf("source-local version = %#v, want v1", version)
|
|
}
|
|
return nil
|
|
},
|
|
}}
|
|
err := test.load(definition)
|
|
var loadErr *LoadError
|
|
if !errors.As(err, &loadErr) {
|
|
t.Fatalf("item placement override error = %T %v, want *LoadError", err, err)
|
|
}
|
|
requireSourceVersionDiagnostic(t, loadErr.Diagnostics(), CodeOverrideVersion, "/catalog/entries")
|
|
if checks != 1 {
|
|
t.Fatalf("source-version checker called %d times, want once", checks)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSourceVersionItemEvidenceUsesProjectedSensitivityPath(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const canary = "PROJECTED-VERSION-EVIDENCE-CANARY"
|
|
secretPath := Keys("catalog", "entries").Index(0).Key("version")
|
|
loaded, err := Load(Mapping("main", map[string]any{"version": canary}), testSecretBinding(), Options[map[string]any]{
|
|
Definition: Definition[map[string]any]{
|
|
Version: &VersionRule{Path: Keys("version"), Check: func(Version, *Validation) error { return nil }},
|
|
Secret: func(path Path) bool { return path.String() == secretPath.String() },
|
|
},
|
|
SourceVersionMode: sourceVersionModeRequired,
|
|
destination: Keys("catalog", "entries"), placementMode: placementModeSequenceItem,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("item source-version load: %v", err)
|
|
}
|
|
sources := loaded.Inspection.Sources()
|
|
if len(sources) != 1 || sources[0].VersionCheck == nil || !sources[0].VersionCheck.Redacted || sources[0].VersionCheck.Version.Text != "" {
|
|
t.Fatalf("projected source-version evidence was not redacted: %#v", sources)
|
|
}
|
|
}
|
|
|
|
func TestSourceVersionLoadChecksNullAndNoSourceMain(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("required actual main absence fails before the checker", func(t *testing.T) {
|
|
t.Parallel()
|
|
checks := 0
|
|
_, err := Load(Bytes("main.yml", []byte("other: value\n")), testSecretBinding(), Options[sourceVersionNullConfig]{
|
|
SourceVersionMode: sourceVersionModeRequired,
|
|
Definition: Definition[sourceVersionNullConfig]{Version: &VersionRule{
|
|
Path: Keys("version"), Check: func(Version, *Validation) error { checks++; return nil },
|
|
}},
|
|
})
|
|
var loadErr *LoadError
|
|
if !errors.As(err, &loadErr) {
|
|
t.Fatalf("missing required main version = %T %v, want *LoadError", err, err)
|
|
}
|
|
requireSourceVersionDiagnostic(t, loadErr.Diagnostics(), CodeVersionError, "/version")
|
|
if checks != 0 {
|
|
t.Fatalf("missing required main invoked checker %d times", checks)
|
|
}
|
|
})
|
|
|
|
t.Run("required null is passed to the checker", func(t *testing.T) {
|
|
t.Parallel()
|
|
checks := 0
|
|
loaded, err := Load(Bytes("main.yml", []byte("version: null\n")), testSecretBinding(), Options[sourceVersionNullConfig]{
|
|
SourceVersionMode: sourceVersionModeRequired,
|
|
Definition: Definition[sourceVersionNullConfig]{Version: &VersionRule{
|
|
Path: Keys("version"),
|
|
Check: func(version Version, _ *Validation) error {
|
|
checks++
|
|
if !version.Present || !version.Null || version.Text != "" {
|
|
return errors.New("unexpected null version")
|
|
}
|
|
return nil
|
|
},
|
|
}},
|
|
})
|
|
if err != nil || loaded == nil || checks != 1 || loaded.Config.Version != nil {
|
|
t.Fatalf("required null load = %#v, callbacks %d, %v", loaded, checks, err)
|
|
}
|
|
})
|
|
|
|
for _, mode := range []sourceVersionMode{sourceVersionModeIfPresent, sourceVersionModeRequired} {
|
|
mode := mode
|
|
t.Run(map[sourceVersionMode]string{sourceVersionModeIfPresent: "if-present", sourceVersionModeRequired: "required"}[mode], func(t *testing.T) {
|
|
t.Parallel()
|
|
checks := 0
|
|
loaded, err := Load(NoSource(), testSecretBinding(), Options[sourceVersionNullConfig]{
|
|
SourceVersionMode: mode,
|
|
Definition: Definition[sourceVersionNullConfig]{Version: &VersionRule{
|
|
Path: Keys("version"), Check: func(version Version, _ *Validation) error {
|
|
checks++
|
|
if version.Present || version.Null {
|
|
return errors.New("NoSource should report absence")
|
|
}
|
|
return nil
|
|
},
|
|
}},
|
|
})
|
|
if err != nil || loaded == nil || checks != 1 || len(testLoaded(loaded).Sources()) != 0 {
|
|
t.Fatalf("NoSource load = %#v, callbacks %d, %v", loaded, checks, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSourceVersionLoadReportsMountedMainErrorsAtLocalPath(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
checks := 0
|
|
_, err := LoadLayered(Input{
|
|
Source: Bytes("main.yml", []byte("version: 5\n")), Destination: Keys("service"),
|
|
}, testSecretBinding(), LayeredOptions[sourceVersionMountedConfig]{
|
|
SourceVersionMode: sourceVersionModeRequired,
|
|
Definition: Definition[sourceVersionMountedConfig]{Version: &VersionRule{
|
|
Path: Keys("version"),
|
|
Check: func(Version, *Validation) error { checks++; return errors.New("rejected") },
|
|
}},
|
|
})
|
|
var loadErr *LoadError
|
|
if !errors.As(err, &loadErr) {
|
|
t.Fatalf("mounted main rejection = %T %v, want *LoadError", err, err)
|
|
}
|
|
requireSourceVersionDiagnostic(t, loadErr.Diagnostics(), CodeVersionHookFailed, "/version")
|
|
if checks != 1 {
|
|
t.Fatalf("mounted main checker calls = %d, want 1", checks)
|
|
}
|
|
}
|
|
|
|
func TestSourceVersionLoadChecksMainBeforeLayeredAcquisition(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
checks := 0
|
|
_, err := LoadLayered(Input{Source: Bytes("main.yml", []byte("version: 5\n"))}, testSecretBinding(), LayeredOptions[sourceVersionNullConfig]{
|
|
SourceVersionMode: sourceVersionModeIfPresent,
|
|
Definition: Definition[sourceVersionNullConfig]{Version: &VersionRule{
|
|
Path: Keys("version"), Check: func(Version, *Validation) error { checks++; return errors.New("rejected") },
|
|
}},
|
|
Underlays: []Underlay{{Inputs: []Input{{Source: Bytes("unread.yml", []byte("[malformed"))}}}},
|
|
})
|
|
var loadErr *LoadError
|
|
if !errors.As(err, &loadErr) {
|
|
t.Fatalf("main rejection = %T %v, want *LoadError", err, err)
|
|
}
|
|
requireSourceVersionDiagnostic(t, loadErr.Diagnostics(), CodeVersionHookFailed, "/version")
|
|
if checks != 1 {
|
|
t.Fatalf("main checker calls = %d, want 1", checks)
|
|
}
|
|
}
|
|
|
|
func TestSourceVersionEnabledBuildsSchemaBeforeMainCallback(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
checks := 0
|
|
_, err := Load(Bytes("main.yml", []byte("version: 5\n")), testSecretBinding(), Options[int]{
|
|
SourceVersionMode: sourceVersionModeIfPresent,
|
|
Definition: Definition[int]{Version: &VersionRule{
|
|
Path: Keys("version"), Check: func(Version, *Validation) error { checks++; return nil },
|
|
}},
|
|
})
|
|
if err == nil || checks != 0 {
|
|
t.Fatalf("invalid schema load = %v, checker calls %d; want schema failure before callback", err, checks)
|
|
}
|
|
}
|
|
|
|
func TestSourceVersionLoadRejectsOptionsBeforeReadingMain(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
checkerCalls := 0
|
|
definition := Definition[sourceVersionNullConfig]{Version: &VersionRule{
|
|
Path: Keys("version"), Check: func(Version, *Validation) error { checkerCalls++; return nil },
|
|
}}
|
|
_, err := Load(Bytes("malformed.yml", []byte("[not valid")), testSecretBinding(), Options[sourceVersionNullConfig]{
|
|
Definition: definition, SourceVersionMode: sourceVersionMode(255),
|
|
})
|
|
var loadErr *LoadError
|
|
if !errors.As(err, &loadErr) {
|
|
t.Fatalf("invalid-mode error = %T %v, want *LoadError", err, err)
|
|
}
|
|
requireSourceVersionDiagnostic(t, loadErr.Diagnostics(), CodeOptionsInvalid, "")
|
|
if checkerCalls != 0 {
|
|
t.Fatalf("invalid mode invoked checker %d times", checkerCalls)
|
|
}
|
|
|
|
_, err = Load(Bytes("malformed.yml", []byte("[not valid")), testSecretBinding(), Options[sourceVersionNullConfig]{SourceVersionMode: sourceVersionModeRequired})
|
|
if !errors.As(err, &loadErr) {
|
|
t.Fatalf("missing-rule error = %T %v, want *LoadError", err, err)
|
|
}
|
|
requireSourceVersionDiagnostic(t, loadErr.Diagnostics(), CodeOptionsInvalid, "")
|
|
|
|
_, err = LoadLayered(Input{Source: Bytes("malformed.yml", []byte("[not valid"))}, testSecretBinding(), LayeredOptions[sourceVersionNullConfig]{
|
|
SourceVersionMode: sourceVersionModeRequired,
|
|
})
|
|
if !errors.As(err, &loadErr) {
|
|
t.Fatalf("layered missing-rule error = %T %v, want *LoadError", err, err)
|
|
}
|
|
requireSourceVersionDiagnostic(t, loadErr.Diagnostics(), CodeOptionsInvalid, "")
|
|
}
|
|
|
|
func TestSourceVersionImportsCheckNestedAndGlobSourcesBeforeComposition(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", `version: main
|
|
configkit:
|
|
imports:
|
|
- glob: fragments/*.yml
|
|
destination: [groups]
|
|
`)
|
|
firstPath := writeImportFile(t, directory, "fragments/a.yml", `version: first
|
|
configkit:
|
|
imports:
|
|
- path: nested/child.yml
|
|
destination: [nested]
|
|
a: {name: A}
|
|
`)
|
|
nestedPath := writeImportFile(t, directory, "fragments/nested/child.yml", "version: nested\nname: N\n")
|
|
secondPath := writeImportFile(t, directory, "fragments/b.yml", "version: second\nb: {name: B}\n")
|
|
|
|
checked := make(map[string]int)
|
|
loaded, err := LoadLayered(Input{
|
|
Source: File(mainPath), Imports: true, Destination: Keys("service"),
|
|
}, testSecretBinding(), LayeredOptions[sourceVersionImportConfig]{
|
|
SourceVersionMode: sourceVersionModeRequired,
|
|
Definition: Definition[sourceVersionImportConfig]{Version: &VersionRule{
|
|
Path: Keys("version"),
|
|
Check: func(version Version, validation *Validation) error {
|
|
checked[version.Text]++
|
|
validation.Warning(Path{}, "accepted")
|
|
return nil
|
|
},
|
|
}},
|
|
})
|
|
if err != nil || loaded == nil {
|
|
var loadErr *LoadError
|
|
if errors.As(err, &loadErr) {
|
|
t.Fatalf("source-local import load = %#v, diagnostics %#v", loaded, loadErr.Diagnostics())
|
|
}
|
|
t.Fatalf("source-local import load = %#v, %v", loaded, err)
|
|
}
|
|
if loaded.Config.Service.Version != "main" || loaded.Config.Service.Groups["a"].Name != "A" ||
|
|
loaded.Config.Service.Groups["b"].Name != "B" || loaded.Config.Service.Groups["nested"].Name != "N" {
|
|
t.Fatalf("mounted source-local imports = %#v", loaded.Config.Service)
|
|
}
|
|
if len(checked) != 4 || checked["main"] != 1 || checked["first"] != 1 || checked["nested"] != 1 || checked["second"] != 1 {
|
|
t.Fatalf("checked declarations = %#v, want each independent source once", checked)
|
|
}
|
|
warnings := testLoaded(loaded).Diagnostics()
|
|
if len(warnings) != 4 {
|
|
t.Fatalf("accepted source warnings = %#v, want one per checked source", warnings)
|
|
}
|
|
for _, warning := range warnings {
|
|
if warning.Code != CodeVersionWarning || warning.Severity != SeverityWarning {
|
|
t.Fatalf("accepted source warning = %#v", warning)
|
|
}
|
|
}
|
|
|
|
wantDestinations := map[string]string{
|
|
canonicalTestPath(t, mainPath): "/service",
|
|
canonicalTestPath(t, firstPath): "/service/groups",
|
|
canonicalTestPath(t, secondPath): "/service/groups",
|
|
canonicalTestPath(t, nestedPath): "/service/groups/nested",
|
|
}
|
|
for _, source := range testLoaded(loaded).Sources() {
|
|
want, found := wantDestinations[source.Path]
|
|
if !found || source.Destination.String() != want {
|
|
t.Errorf("source %q destination = %q, want %q", source.Path, source.Destination.String(), want)
|
|
}
|
|
delete(wantDestinations, source.Path)
|
|
}
|
|
if len(wantDestinations) != 0 {
|
|
t.Errorf("sources not retained: %#v", wantDestinations)
|
|
}
|
|
}
|
|
|
|
func TestSourceVersionImportsRejectHiddenAndMissingDeclarations(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("rejected child is not hidden by the main declaration", func(t *testing.T) {
|
|
t.Parallel()
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", "version: main\nconfigkit: {imports: [{path: child.yml}]}\n")
|
|
writeImportFile(t, directory, "child.yml", "version: rejected\n")
|
|
checks := 0
|
|
_, err := Load(File(mainPath), testSecretBinding(), Options[sourceVersionNullConfig]{
|
|
Imports: true, SourceVersionMode: sourceVersionModeIfPresent,
|
|
Definition: Definition[sourceVersionNullConfig]{Version: &VersionRule{
|
|
Path: Keys("version"), Check: func(version Version, _ *Validation) error {
|
|
checks++
|
|
if version.Text == "rejected" {
|
|
return errors.New("rejected")
|
|
}
|
|
return nil
|
|
},
|
|
}},
|
|
})
|
|
var loadErr *LoadError
|
|
if !errors.As(err, &loadErr) {
|
|
t.Fatalf("rejected child error = %T %v, want *LoadError", err, err)
|
|
}
|
|
diagnostic := requireSourceVersionDiagnostic(t, loadErr.Diagnostics(), CodeVersionHookFailed, "/version")
|
|
if diagnostic.Primary == nil || diagnostic.Primary.SourceID != "source-2" || checks != 2 {
|
|
t.Fatalf("rejected child diagnostic/callbacks = %#v, %d", diagnostic, checks)
|
|
}
|
|
})
|
|
|
|
t.Run("required child omission reports its nearest source location", func(t *testing.T) {
|
|
t.Parallel()
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", "version: main\nconfigkit: {imports: [{path: child.yml}]}\n")
|
|
writeImportFile(t, directory, "child.yml", "name: child\n")
|
|
checks := 0
|
|
_, err := Load(File(mainPath), testSecretBinding(), Options[sourceVersionNullConfig]{
|
|
Imports: true, SourceVersionMode: sourceVersionModeRequired,
|
|
Definition: Definition[sourceVersionNullConfig]{Version: &VersionRule{
|
|
Path: Keys("version"), Check: func(Version, *Validation) error { checks++; return nil },
|
|
}},
|
|
})
|
|
var loadErr *LoadError
|
|
if !errors.As(err, &loadErr) {
|
|
t.Fatalf("required child omission = %T %v, want *LoadError", err, err)
|
|
}
|
|
diagnostic := requireSourceVersionDiagnostic(t, loadErr.Diagnostics(), CodeVersionError, "/version")
|
|
if diagnostic.Primary == nil || diagnostic.Primary.SourceID != "source-2" || diagnostic.Primary.Line != 1 || checks != 1 {
|
|
t.Fatalf("required omission diagnostic/callbacks = %#v, %d", diagnostic, checks)
|
|
}
|
|
})
|
|
|
|
t.Run("optional missing child remains absent", func(t *testing.T) {
|
|
t.Parallel()
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", `version: main
|
|
configkit:
|
|
imports:
|
|
- path: missing.yml
|
|
`)
|
|
checks := 0
|
|
loaded, err := Load(File(mainPath), testSecretBinding(), Options[sourceVersionNullConfig]{
|
|
Imports: true, SourceVersionMode: sourceVersionModeRequired,
|
|
Definition: Definition[sourceVersionNullConfig]{Version: &VersionRule{
|
|
Path: Keys("version"), Check: func(Version, *Validation) error { checks++; return nil },
|
|
}},
|
|
})
|
|
if err != nil || loaded == nil || checks != 1 || len(testLoaded(loaded).Sources()) != 1 {
|
|
t.Fatalf("optional missing source = %#v, checks %d, %v", loaded, checks, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSourceVersionUnderlaysCheckDistinctMountedDeclarations(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var checked []string
|
|
definition := Definition[map[string]any]{Version: &VersionRule{
|
|
Path: Keys("meta", "version"),
|
|
Check: func(version Version, validation *Validation) error {
|
|
checked = append(checked, version.Text)
|
|
if version.Text == "peer-a" || version.Text == "peer-c" {
|
|
validation.Warning(Path{}, "accepted with advisory")
|
|
}
|
|
return nil
|
|
},
|
|
}}
|
|
main := Input{
|
|
Source: Bytes("main", []byte("meta: {version: main-v}\nservice: primary\n")),
|
|
Destination: Keys("service"),
|
|
}
|
|
first := Input{
|
|
Source: Bytes("peer-a", []byte("meta:\n version: peer-a\nvalues: {first: one}\n")),
|
|
Destination: Keys("defaults", "first"),
|
|
}
|
|
second := Input{
|
|
Source: Bytes("peer-b", []byte("meta:\n version: peer-b\nvalues: {second: two}\n")),
|
|
Destination: Keys("defaults", "second"),
|
|
}
|
|
third := Input{
|
|
Source: Bytes("peer-c", []byte("meta:\n version: peer-c\nvalues: {third: three}\n")),
|
|
Destination: Keys("defaults", "third"),
|
|
}
|
|
loaded, err := LoadLayered(main, testSecretBinding(), LayeredOptions[map[string]any]{
|
|
Definition: definition, SourceVersionMode: sourceVersionModeRequired,
|
|
Underlays: []Underlay{{Inputs: []Input{first, second}}, {Inputs: []Input{third}}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("LoadLayered: %v", err)
|
|
}
|
|
if len(checked) != 4 {
|
|
t.Fatalf("checked declarations = %#v, want main and all underlays", checked)
|
|
}
|
|
for _, want := range []string{"main-v", "peer-a", "peer-b", "peer-c"} {
|
|
found := false
|
|
for _, got := range checked {
|
|
found = found || got == want
|
|
}
|
|
if !found {
|
|
t.Errorf("checked declarations %#v do not include %q", checked, want)
|
|
}
|
|
}
|
|
if len(loaded.Inspection.Diagnostics()) != 2 || loaded.Inspection.Diagnostics()[0].Code != CodeVersionWarning || loaded.Inspection.Diagnostics()[1].Code != CodeVersionWarning {
|
|
t.Fatalf("accepted underlay warning was not retained: %#v", loaded.Inspection.Diagnostics())
|
|
}
|
|
sources := loaded.Inspection.Sources()
|
|
if len(sources) != 4 || sources[1].Layer != LayerUnderlay || sources[1].UnderlayIndex != 1 || sources[2].Layer != LayerUnderlay || sources[2].UnderlayIndex != 1 || sources[3].Layer != LayerUnderlay || sources[3].UnderlayIndex != 2 {
|
|
t.Fatalf("underlay source records = %#v", sources)
|
|
}
|
|
defaults, ok := loaded.Config["defaults"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("defaults = %#v", loaded.Config["defaults"])
|
|
}
|
|
firstValue, ok := defaults["first"].(map[string]any)
|
|
if !ok || firstValue["values"] == nil {
|
|
t.Fatalf("first mounted contribution = %#v", defaults["first"])
|
|
}
|
|
if meta, ok := firstValue["meta"].(map[string]any); !ok || len(meta) != 0 {
|
|
t.Fatalf("consumed nested declaration did not preserve its empty ancestor: %#v", firstValue["meta"])
|
|
}
|
|
}
|
|
|
|
func TestSourceVersionUnderlaysRejectDeclarationsHiddenByMain(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var checked []string
|
|
loaded, err := LoadLayered(Input{Source: Bytes("main", []byte("meta: {version: main-v}\nservice: primary\n"))}, testSecretBinding(), LayeredOptions[map[string]any]{
|
|
Definition: Definition[map[string]any]{Version: &VersionRule{
|
|
Path: Keys("meta", "version"),
|
|
Check: func(version Version, _ *Validation) error {
|
|
checked = append(checked, version.Text)
|
|
if version.Text == "rejected-peer" {
|
|
return errors.New("peer declaration is not accepted")
|
|
}
|
|
return nil
|
|
},
|
|
}},
|
|
SourceVersionMode: sourceVersionModeIfPresent,
|
|
Underlays: []Underlay{{Inputs: []Input{{Source: Bytes("underlay", []byte("meta: {version: rejected-peer}\nservice: shadowed\n"))}}}},
|
|
})
|
|
if err == nil || loaded != nil {
|
|
t.Fatalf("LoadLayered = %#v, %v, want hidden underlay rejection", loaded, err)
|
|
}
|
|
loadErr, ok := err.(*LoadError)
|
|
if !ok || len(loadErr.Diagnostics()) != 1 || loadErr.Diagnostics()[0].Code != CodeVersionHookFailed {
|
|
t.Fatalf("underlay rejection diagnostics = %#v", err)
|
|
}
|
|
if len(checked) != 2 || checked[0] != "main-v" || checked[1] != "rejected-peer" {
|
|
t.Fatalf("checked declarations = %#v", checked)
|
|
}
|
|
}
|
|
|
|
func TestSourceVersionUnderlaysRequireActualHeadersButSkipAbsentSources(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
newOptions := func(underlays []Underlay) LayeredOptions[map[string]any] {
|
|
return LayeredOptions[map[string]any]{
|
|
Definition: Definition[map[string]any]{Version: &VersionRule{
|
|
Path: Keys("meta", "version"),
|
|
Check: func(Version, *Validation) error { return nil },
|
|
}},
|
|
SourceVersionMode: sourceVersionModeRequired,
|
|
Underlays: underlays,
|
|
}
|
|
}
|
|
main := Input{Source: Bytes("main", []byte("meta: {version: main-v}\n"))}
|
|
|
|
t.Run("required absence", func(t *testing.T) {
|
|
loaded, err := LoadLayered(main, testSecretBinding(), newOptions([]Underlay{{Inputs: []Input{{Source: Bytes("missing", []byte("value: present\n"))}}}}))
|
|
if err == nil || loaded != nil {
|
|
t.Fatalf("LoadLayered = %#v, %v, want missing underlay declaration", loaded, err)
|
|
}
|
|
loadErr, ok := err.(*LoadError)
|
|
if !ok || len(loadErr.Diagnostics()) != 1 || loadErr.Diagnostics()[0].Code != CodeVersionError {
|
|
t.Fatalf("missing underlay diagnostics = %#v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("empty group and NoSource", func(t *testing.T) {
|
|
loaded, err := LoadLayered(main, testSecretBinding(), newOptions([]Underlay{{}, {Inputs: []Input{{Source: NoSource()}}}}))
|
|
if err != nil {
|
|
t.Fatalf("LoadLayered with absent contributions: %v", err)
|
|
}
|
|
if sources := loaded.Inspection.Sources(); len(sources) != 1 {
|
|
t.Fatalf("empty groups and NoSource created source records: %#v", sources)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSourceVersionUnderlayImportsKeepLocalHeadersAndMountedDestinations(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
directory := t.TempDir()
|
|
underlayPath := writeImportFile(t, directory, "underlay.yml", `meta: {version: peer-root}
|
|
configkit:
|
|
imports:
|
|
- path: nested.yml
|
|
destination: [nested]
|
|
root: retained
|
|
`)
|
|
writeImportFile(t, directory, "nested.yml", "meta: {version: peer-nested}\nname: nested\n")
|
|
var checked []string
|
|
loaded, err := LoadLayered(Input{
|
|
Source: Bytes("main", []byte("meta: {version: main-v}\n")), Destination: Keys("service"),
|
|
}, testSecretBinding(), LayeredOptions[map[string]any]{
|
|
Definition: Definition[map[string]any]{Version: &VersionRule{
|
|
Path: Keys("meta", "version"),
|
|
Check: func(version Version, _ *Validation) error {
|
|
checked = append(checked, version.Text)
|
|
return nil
|
|
},
|
|
}},
|
|
SourceVersionMode: sourceVersionModeRequired,
|
|
Underlays: []Underlay{{Inputs: []Input{{
|
|
Source: File(underlayPath), Imports: true, Destination: Keys("defaults"),
|
|
}}}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("LoadLayered: %v", err)
|
|
}
|
|
if len(checked) != 3 {
|
|
t.Fatalf("checked source declarations = %#v", checked)
|
|
}
|
|
for _, want := range []string{"main-v", "peer-root", "peer-nested"} {
|
|
found := false
|
|
for _, got := range checked {
|
|
found = found || got == want
|
|
}
|
|
if !found {
|
|
t.Errorf("checked source declarations %#v do not include %q", checked, want)
|
|
}
|
|
}
|
|
sources := loaded.Inspection.Sources()
|
|
if len(sources) != 3 || sources[1].Path != filepath.Clean(underlayPath) || sources[2].Destination.String() != "/defaults/nested" {
|
|
t.Fatalf("underlay and nested import records = %#v", sources)
|
|
}
|
|
if sources[1].Layer != LayerUnderlay || sources[1].UnderlayIndex != 1 || sources[2].Layer != LayerUnderlay || sources[2].UnderlayIndex != 1 {
|
|
t.Fatalf("underlay and nested import tiers = %#v", sources)
|
|
}
|
|
defaults, ok := loaded.Config["defaults"].(map[string]any)
|
|
if !ok || defaults["root"] != "retained" {
|
|
t.Fatalf("mounted underlay root = %#v", loaded.Config["defaults"])
|
|
}
|
|
nested, ok := defaults["nested"].(map[string]any)
|
|
if !ok || nested["name"] != "nested" {
|
|
t.Fatalf("mounted nested import = %#v", defaults["nested"])
|
|
}
|
|
}
|
|
|
|
func TestSourceVersionUnderlaysProtectRemainingMountedPayload(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaded, err := LoadLayered(Input{
|
|
Source: Bytes("main", []byte("meta: {version: main-v}\n")), Destination: Keys("service"),
|
|
}, testSecretBinding(), LayeredOptions[map[string]any]{
|
|
Definition: Definition[map[string]any]{Version: &VersionRule{
|
|
Path: Keys("meta", "version"), Check: func(Version, *Validation) error { return nil },
|
|
}},
|
|
SourceVersionMode: sourceVersionModeRequired,
|
|
Underlays: []Underlay{{Inputs: []Input{{Source: Bytes("underlay", []byte(
|
|
"meta: {version: accepted}\nservice: {meta: {version: payload}}\n",
|
|
))}}}},
|
|
})
|
|
if err == nil || loaded != nil {
|
|
t.Fatalf("LoadLayered = %#v, %v, want protected remaining payload rejection", loaded, err)
|
|
}
|
|
loadErr, ok := err.(*LoadError)
|
|
if !ok || len(loadErr.Diagnostics()) != 1 || loadErr.Diagnostics()[0].Code != CodeUnderlayVersion {
|
|
t.Fatalf("remaining payload diagnostics = %#v", err)
|
|
}
|
|
}
|
|
|
|
func TestSourceVersionEvidencePropagatesThroughRecordedResolutionDependency(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type credentials struct {
|
|
Password string `yaml:"password"`
|
|
Public string `yaml:"public"`
|
|
}
|
|
type configuration struct {
|
|
Credentials credentials `yaml:"credentials"`
|
|
Derived string `yaml:"derived"`
|
|
}
|
|
load := func(header string) *Loaded[configuration] {
|
|
t.Helper()
|
|
loaded, err := LoadLayered(Input{
|
|
Source: Bytes("main", []byte("credentials: {password: main-value}\n")),
|
|
}, testSecretBinding(), LayeredOptions[configuration]{
|
|
SourceVersionMode: sourceVersionModeIfPresent,
|
|
Underlays: []Underlay{{Inputs: []Input{{Source: Bytes("underlay", []byte(
|
|
"credentials:\n version: "+header+"\n public: visible-neighbor\n",
|
|
))}}}},
|
|
Definition: Definition[configuration]{
|
|
Version: &VersionRule{
|
|
Path: Keys("credentials", "version"),
|
|
Check: func(Version, *Validation) error { return nil },
|
|
},
|
|
Secret: func(path Path) bool {
|
|
return path.String() == "/credentials/version"
|
|
},
|
|
Resolve: func(value *configuration, resolution *Resolution) error {
|
|
value.Derived = value.Credentials.Password
|
|
resolution.Record(Keys("derived"), ChangeNormalization, "derive from credentials", Keys("credentials"))
|
|
return nil
|
|
},
|
|
},
|
|
})
|
|
if err != nil || loaded == nil {
|
|
t.Fatalf("LoadLayered with header %q = %#v, %v", header, loaded, err)
|
|
}
|
|
return loaded
|
|
}
|
|
|
|
left, right := load("202"), load("303")
|
|
if left.Config.Derived != "main-value" || right.Config.Derived != "main-value" {
|
|
t.Fatalf("derived values = %q, %q", left.Config.Derived, right.Config.Derived)
|
|
}
|
|
if !left.Inspection.secrets["/derived"] || !right.Inspection.secrets["/derived"] {
|
|
t.Fatalf("evidence did not activate recorded dependency closure: left=%#v right=%#v", left.Inspection.secrets, right.Inspection.secrets)
|
|
}
|
|
derived, found := left.Inspection.Explain(Keys("derived"))
|
|
if !found || !derived.Effective.Redacted {
|
|
t.Fatalf("derived explanation was not redacted: %#v, found=%v", derived, found)
|
|
}
|
|
public, found := left.Inspection.Explain(Keys("credentials", "public"))
|
|
if !found || public.Effective.Redacted || public.Effective.YAML != "visible-neighbor\n" {
|
|
t.Fatalf("neighboring payload = %#v, found=%v", public, found)
|
|
}
|
|
headerPath := Keys("credentials", "version")
|
|
if _, found := left.Inspection.Explain(headerPath); found {
|
|
t.Fatal("consumed declaration created an effective or historical explanation path")
|
|
}
|
|
if _, found := left.Inspection.Metadata().Lookup(headerPath); found {
|
|
t.Fatal("consumed declaration created a metadata path")
|
|
}
|
|
if differences, err := Diff(left.Inspection, right.Inspection); err != nil || len(differences) != 0 {
|
|
t.Fatalf("different consumed headers changed effective diff: %#v, %v", differences, err)
|
|
}
|
|
}
|
|
|
|
func TestSourceVersionSelectedOverlayConsumesHeaderAndRetainsWarning(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", "schema: {version: main-v}\nsettings: {kept: base}\nitems: [{id: base}]\n")
|
|
overlayPath := writeImportFile(t, directory, "overlays/testing.yml", `schema: {version: overlay-v}
|
|
configkit:
|
|
sequences:
|
|
- destination: [items]
|
|
key: id
|
|
mode: append
|
|
settings: {changed: overlay}
|
|
items: [{id: overlay}]
|
|
`)
|
|
var checked []string
|
|
loaded, err := Load(File(mainPath), testSecretBinding(), Options[map[string]any]{
|
|
Overlay: "testing", SourceVersionMode: sourceVersionModeRequired,
|
|
Definition: Definition[map[string]any]{Version: &VersionRule{
|
|
Path: Keys("schema", "version"),
|
|
Check: func(version Version, validation *Validation) error {
|
|
checked = append(checked, version.Text)
|
|
if version.Text == "overlay-v" {
|
|
validation.Warning(Path{}, "overlay accepted with advisory")
|
|
}
|
|
return nil
|
|
},
|
|
}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if len(checked) != 2 || checked[0] != "main-v" || checked[1] != "overlay-v" {
|
|
t.Fatalf("checked source declarations = %#v", checked)
|
|
}
|
|
if loaded.Config["schema"].(map[string]any)["version"] != "main-v" || loaded.Config["settings"].(map[string]any)["changed"] != "overlay" {
|
|
t.Fatalf("overlay payload = %#v", loaded.Config)
|
|
}
|
|
if _, found := loaded.Config["configkit"]; found || len(loaded.Config["items"].([]any)) != 2 {
|
|
t.Fatalf("overlay declarations or collection values = %#v", loaded.Config)
|
|
}
|
|
diagnostics := loaded.Inspection.Diagnostics()
|
|
if len(diagnostics) != 1 || diagnostics[0].Code != CodeVersionWarning {
|
|
t.Fatalf("overlay warning was not retained: %#v", diagnostics)
|
|
}
|
|
sources := loaded.Inspection.Sources()
|
|
if len(sources) != 2 || sources[1].Path != filepath.Clean(overlayPath) || sources[1].Layer != LayerOverlay {
|
|
t.Fatalf("overlay source records = %#v", sources)
|
|
}
|
|
}
|
|
|
|
func TestSourceVersionSelectedOverlayRequiresAndChecksItsDeclaration(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("required absence", func(t *testing.T) {
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", "schema: {version: main-v}\n")
|
|
writeImportFile(t, directory, "overlays/testing.yml", "settings: {changed: overlay}\n")
|
|
loaded, err := Load(File(mainPath), testSecretBinding(), Options[map[string]any]{
|
|
Overlay: "testing", SourceVersionMode: sourceVersionModeRequired,
|
|
Definition: Definition[map[string]any]{Version: &VersionRule{
|
|
Path: Keys("schema", "version"), Check: func(Version, *Validation) error { return nil },
|
|
}},
|
|
})
|
|
if err == nil || loaded != nil {
|
|
t.Fatalf("Load = %#v, %v, want required overlay declaration error", loaded, err)
|
|
}
|
|
loadErr, ok := err.(*LoadError)
|
|
if !ok || len(loadErr.Diagnostics()) != 1 || loadErr.Diagnostics()[0].Code != CodeVersionError {
|
|
t.Fatalf("missing overlay diagnostics = %#v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("checker rejection", func(t *testing.T) {
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", "schema: {version: main-v}\n")
|
|
writeImportFile(t, directory, "overlays/testing.yml", "schema: {version: rejected-overlay}\nsettings: {changed: hidden}\n")
|
|
var checked []string
|
|
loaded, err := Load(File(mainPath), testSecretBinding(), Options[map[string]any]{
|
|
Overlay: "testing", SourceVersionMode: sourceVersionModeIfPresent,
|
|
Definition: Definition[map[string]any]{Version: &VersionRule{
|
|
Path: Keys("schema", "version"),
|
|
Check: func(version Version, _ *Validation) error {
|
|
checked = append(checked, version.Text)
|
|
if version.Text == "rejected-overlay" {
|
|
return errors.New("overlay rejected")
|
|
}
|
|
return nil
|
|
},
|
|
}},
|
|
})
|
|
if err == nil || loaded != nil {
|
|
t.Fatalf("Load = %#v, %v, want overlay rejection", loaded, err)
|
|
}
|
|
loadErr, ok := err.(*LoadError)
|
|
if !ok || len(loadErr.Diagnostics()) != 1 || loadErr.Diagnostics()[0].Code != CodeVersionHookFailed {
|
|
t.Fatalf("overlay rejection diagnostics = %#v", err)
|
|
}
|
|
if len(checked) != 2 || checked[0] != "main-v" || checked[1] != "rejected-overlay" {
|
|
t.Fatalf("checked source declarations = %#v", checked)
|
|
}
|
|
})
|
|
|
|
t.Run("structural validation precedes declaration processing", func(t *testing.T) {
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", "schema: {version: main-v}\n")
|
|
writeImportFile(t, directory, "overlays/testing.yml", "configkit: {imports: true}\nschema: {version: overlay-v}\n")
|
|
checks := 0
|
|
loaded, err := Load(File(mainPath), testSecretBinding(), Options[map[string]any]{
|
|
Overlay: "testing", SourceVersionMode: sourceVersionModeRequired,
|
|
Definition: Definition[map[string]any]{Version: &VersionRule{
|
|
Path: Keys("schema", "version"), Check: func(Version, *Validation) error { checks++; return nil },
|
|
}},
|
|
})
|
|
if err == nil || loaded != nil {
|
|
t.Fatalf("Load = %#v, %v, want structural overlay rejection", loaded, err)
|
|
}
|
|
loadErr, ok := err.(*LoadError)
|
|
if !ok || len(loadErr.Diagnostics()) != 1 || loadErr.Diagnostics()[0].Code != CodeOverlayDirective {
|
|
t.Fatalf("structural overlay diagnostics = %#v", err)
|
|
}
|
|
if checks != 1 {
|
|
t.Fatalf("version checker calls = %d, want main only", checks)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSourceVersionOverlayLeavesCLIOverridesHeaderFree(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
checks := 0
|
|
loaded, err := Load(Bytes("main", []byte("schema: {version: main-v}\nsettings: {base: value}\n")), testSecretBinding(), Options[map[string]any]{
|
|
SourceVersionMode: sourceVersionModeRequired,
|
|
Definition: Definition[map[string]any]{Version: &VersionRule{
|
|
Path: Keys("schema", "version"),
|
|
Check: func(Version, *Validation) error {
|
|
checks++
|
|
return nil
|
|
},
|
|
}},
|
|
Overrides: []Override{{Path: Keys("settings", "cli"), Value: "supplied", Flag: "--settings.cli"}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Load with CLI override: %v", err)
|
|
}
|
|
if checks != 1 || loaded.Config["settings"].(map[string]any)["cli"] != "supplied" {
|
|
t.Fatalf("checker count/configuration = %d, %#v", checks, loaded.Config)
|
|
}
|
|
}
|
|
|
|
func TestSourceVersionOverlayProtectsMountedMainAndPreservesSequenceCoordinates(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("mounted main ownership", func(t *testing.T) {
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", "meta: {version: main-v}\n")
|
|
writeImportFile(t, directory, "overlays/testing.yml", "meta: {version: overlay-v}\nservice: {meta: {version: payload}}\n")
|
|
_, err := LoadLayered(Input{Source: File(mainPath), Destination: Keys("service")}, testSecretBinding(), LayeredOptions[map[string]any]{
|
|
Overlay: "testing", OverlayFilesystem: &FilesystemContext{BaseDir: directory, Root: directory},
|
|
SourceVersionMode: sourceVersionModeRequired,
|
|
Definition: Definition[map[string]any]{Version: &VersionRule{
|
|
Path: Keys("meta", "version"), Check: func(Version, *Validation) error { return nil },
|
|
}},
|
|
})
|
|
loadErr, ok := err.(*LoadError)
|
|
if !ok || len(loadErr.Diagnostics()) != 1 || loadErr.Diagnostics()[0].Code != CodeOverlayVersion {
|
|
t.Fatalf("mounted overlay ownership error = %#v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("sequence location remains source-local after deletion", func(t *testing.T) {
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", "base: true\n")
|
|
writeImportFile(t, directory, "overlays/testing.yml", "versions:\n - before\n - overlay-version\n - after\n")
|
|
main, diagnostics := parseSource(File(mainPath), "source-main", defaultParseLimits())
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("parse main: %#v", diagnostics)
|
|
}
|
|
localRule := &VersionRule{Path: Keys("versions").Index(1), Check: func(version Version, _ *Validation) error {
|
|
if version.Text != "overlay-version" {
|
|
t.Errorf("checked version = %#v", version)
|
|
}
|
|
return nil
|
|
}}
|
|
effectiveRule := effectiveSourceVersionRule(localRule, Keys("service"), sourceVersionModeRequired)
|
|
state := &sourceVersionState{localRule: localRule, effectiveRule: effectiveRule, mode: sourceVersionModeRequired}
|
|
base, compositionDiagnostics := composeDocuments(&document.Node{Kind: document.Mapping})
|
|
if len(compositionDiagnostics) != 0 {
|
|
t.Fatalf("compose base: %#v", compositionDiagnostics)
|
|
}
|
|
assembly := newAssemblyAccumulator(base)
|
|
_, diagnostics = loadSelectedOverlayIntoWithVersion(
|
|
File(mainPath), nil, main, assembly, sourceInfos(main.source), "testing", effectiveRule, defaultImportLimits(), state,
|
|
)
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("load selected overlay: %#v", diagnostics)
|
|
}
|
|
if len(state.evidence) != 1 || state.evidence[0].resolvedPath.String() != "/versions/1" || state.evidence[0].location == nil || state.evidence[0].location.Line != 3 {
|
|
t.Fatalf("sequence evidence lost original coordinates: %#v", state.evidence)
|
|
}
|
|
versions, found := documentAtPath(assembly.current.root, Keys("versions"))
|
|
if !found || versions.Kind != document.Sequence || len(versions.Sequence) != 2 || versions.Sequence[0].Text != "before" || versions.Sequence[1].Text != "after" {
|
|
t.Fatalf("remaining overlay sequence = %#v", versions)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSourceVersionImportAccountingIncludesConsumedHeaderNodes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: child.yml}]}\n")
|
|
childPath := writeImportFile(t, directory, "child.yml", "version: accepted\nname: child\n")
|
|
main := parseImportMain(t, mainPath)
|
|
child, diagnostics := parseSource(File(childPath), "source-child", defaultParseLimits())
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("child parse diagnostics = %#v", diagnostics)
|
|
}
|
|
rule := &VersionRule{Path: Keys("version"), Check: func(Version, *Validation) error { return nil }}
|
|
state := &sourceVersionState{localRule: rule, effectiveRule: rule, mode: sourceVersionModeRequired}
|
|
wantNodes := countInputNodes(main.document) + countInputNodes(child.document)
|
|
wantBytes := main.bytes + child.bytes
|
|
limits := defaultImportLimits()
|
|
limits.bytes = wantBytes
|
|
limits.nodes = wantNodes
|
|
|
|
accounting := &importAccounting{}
|
|
composed, sources, foundDiagnostics := loadImportedDocumentsTrackedWithVersion(
|
|
File(mainPath), nil, main, Path{}, rule, limits, accounting, nil, nil, state,
|
|
)
|
|
if len(foundDiagnostics) != 0 || composed == nil || len(sources) != 2 || accounting.nodes != wantNodes || accounting.bytes != wantBytes {
|
|
t.Fatalf("exact raw byte/node accounting = %#v, %d/%d, want %d/%d, %#v", composed, accounting.bytes, accounting.nodes, wantBytes, wantNodes, foundDiagnostics)
|
|
}
|
|
if len(state.evidence) != 1 || state.evidence[0].sourceID != "source-2" || !state.evidence[0].checked || state.evidence[0].version.Text != "accepted" {
|
|
t.Fatalf("acquired child evidence = %#v", state.evidence)
|
|
}
|
|
if _, present := documentAtPath(composed.root, Keys("version")); present {
|
|
t.Fatalf("consumed child header remains in application document: %#v", composed.root)
|
|
}
|
|
|
|
below := defaultImportLimits()
|
|
below.nodes = wantNodes - 1
|
|
accounting = &importAccounting{}
|
|
composed, _, foundDiagnostics = loadImportedDocumentsTrackedWithVersion(
|
|
File(mainPath), nil, main, Path{}, rule, below, accounting, nil, nil,
|
|
&sourceVersionState{localRule: rule, effectiveRule: rule, mode: sourceVersionModeRequired},
|
|
)
|
|
if composed != nil || len(foundDiagnostics) != 1 || foundDiagnostics[0].Code != CodeImportLimitExceeded {
|
|
t.Fatalf("raw header nodes were not charged: %#v, %#v", composed, foundDiagnostics)
|
|
}
|
|
|
|
below = defaultImportLimits()
|
|
below.bytes = wantBytes - 1
|
|
accounting = &importAccounting{}
|
|
composed, _, foundDiagnostics = loadImportedDocumentsTrackedWithVersion(
|
|
File(mainPath), nil, main, Path{}, rule, below, accounting, nil, nil,
|
|
&sourceVersionState{localRule: rule, effectiveRule: rule, mode: sourceVersionModeRequired},
|
|
)
|
|
if composed != nil || len(foundDiagnostics) != 1 || foundDiagnostics[0].Code != CodeImportLimitExceeded {
|
|
t.Fatalf("raw header bytes were not charged: %#v, %#v", composed, foundDiagnostics)
|
|
}
|
|
}
|
|
|
|
func mustParseSourceVersionPath(t *testing.T, input string) Path {
|
|
t.Helper()
|
|
path, err := ParsePath(input)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return path
|
|
}
|