170 lines
8.5 KiB
Go
170 lines
8.5 KiB
Go
package engine
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
|
|
"go.maximumdirect.net/configkit/internal/document"
|
|
)
|
|
|
|
func TestPostDecodeOverrideContextOrdersDecodeCLIAndResolutionEvidence(t *testing.T) {
|
|
t.Parallel()
|
|
type item struct {
|
|
Name string `yaml:"name"`
|
|
Enabled bool `yaml:"enabled"`
|
|
}
|
|
type config struct {
|
|
Version string `yaml:"version"`
|
|
Rate float64 `yaml:"rate"`
|
|
Count int `yaml:"count"`
|
|
Items []item `yaml:"items"`
|
|
APIKey Secret `yaml:"api_key"`
|
|
Derived int `yaml:"derived"`
|
|
}
|
|
defaults := config{Version: "default", Rate: 2, Count: 7, Items: []item{{Name: "default"}}, APIKey: SecretEnv("DEFAULT_SECRET")}
|
|
structural := parseDocumentForDecode(t, "version: 1\nrate: 1.20\ncount: 5\nitems: [{name: old, enabled: true}]\napi_key: BASE_SECRET\n")
|
|
setOverrideTestDocumentSource(structural, "source-1")
|
|
versionPath := Keys("version")
|
|
schema, schemaIssue := buildSchema(reflect.TypeFor[config](), testSecretBinding())
|
|
if schemaIssue != nil {
|
|
t.Fatalf("buildSchema() issue = %#v", schemaIssue)
|
|
}
|
|
decoded, issues := decodeConfigurationWithContext(schema, reflect.ValueOf(defaults), structural, decodeContext{versionPath: &versionPath})
|
|
if len(issues) != 0 {
|
|
t.Fatalf("decodeConfigurationWithContext() issues = %#v", issues)
|
|
}
|
|
sources := []SourceInfo{{ID: "source-1", Label: "main input", Layer: LayerMain, Kind: SourceKindMapping}}
|
|
contexts := map[string]ResourceContext{"source-1": {BaseDir: "/config"}}
|
|
input := assembledInput{document: structural, sources: sources, contexts: contexts}
|
|
batch, diagnostics := preparePostStructuralOverrides(overridePreparationInput{root: structural}, sources, []Override{
|
|
{Path: Keys("rate"), Value: 1.2, Flag: "--rate"},
|
|
{Path: Keys("count"), Value: 5, Flag: "--count"},
|
|
{Path: Keys("items"), Value: []any{map[string]any{"name": "new"}}, Flag: "--items"},
|
|
{Path: Keys("api_key"), Value: "CLI_SECRET", Flag: "--api-key"},
|
|
}, nil, defaultImportLimits())
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("preparePostStructuralOverrides() diagnostics = %#v", diagnostics)
|
|
}
|
|
assigned, issues := applyDecodedOverrides(schema, decoded, batch, decodeContext{versionPath: &versionPath})
|
|
if len(issues) != 0 {
|
|
t.Fatalf("applyDecodedOverrides() issues = %#v", issues)
|
|
}
|
|
prepared := preparePostDecodeOverrideContext(input, decoded, assigned)
|
|
if prepared.metadata.historyBuilder == nil || !prepared.metadata.historyBuilder.prefixReady {
|
|
t.Fatal("post-decode handoff did not retain the completed history prefix")
|
|
}
|
|
if prepared.resolution.metadata.historyBuilder != nil || prepared.resolution.metadata.historyModel != nil {
|
|
t.Fatal("resolution entry view borrowed mutable or historical state")
|
|
}
|
|
if prepared.decoded.final == assigned.final || prepared.referenceInput == assigned.final {
|
|
t.Fatal("post-CLI configuration and located resolution views are not detached")
|
|
}
|
|
if prepared.metadata.fields["/count"].Origin.Layer != LayerCLI || !prepared.metadata.fields["/count"].Explicit {
|
|
t.Fatalf("equal-value CLI metadata = %#v", prepared.metadata.fields["/count"])
|
|
}
|
|
if prepared.decoded.origins["/count"].Layer != LayerCLI || prepared.decoded.presence["/count"].location.SourceID != batch.overrides[1].source.ID {
|
|
t.Fatalf("post-CLI metadata maps = %#v / %#v", prepared.decoded.origins["/count"], prepared.decoded.presence["/count"])
|
|
}
|
|
if _, explicit := prepared.decoded.presence["/items/0/enabled"]; explicit {
|
|
t.Fatal("replaced list retained discarded-child presence")
|
|
}
|
|
if prepared.resolution.contexts["source-1"].BaseDir != "/config" {
|
|
t.Fatalf("resolution source contexts = %#v", prepared.resolution.contexts)
|
|
}
|
|
if info, ok := prepared.resolution.lookup(Keys("count")); !ok || !info.Explicit || info.Origin.Layer != LayerCLI {
|
|
t.Fatalf("resolution entry count = %#v, %v", info, ok)
|
|
}
|
|
countSource := batch.overrides[1].source.ID
|
|
if location, ok := metadataFieldLocation(prepared.metadata, Keys("count"), Origin{Layer: LayerDefault}); !ok || location.SourceID != countSource {
|
|
t.Fatalf("pre-resolution historical location = %#v, %v", location, ok)
|
|
}
|
|
cliSource := batch.overrides[3].source.ID
|
|
if source, ok := prepared.resolution.source(cliSource); !ok || source.Layer != LayerCLI || source.Label != "--api-key" {
|
|
t.Fatalf("resolution CLI source = %#v, %v", source, ok)
|
|
}
|
|
|
|
resolvedContext, resolveDiagnostics := resolveLoadInput(schema, Definition[config]{Resolve: func(value *config, resolution *Resolution) error {
|
|
value.Count = 6
|
|
resolution.RecordNormalization(Keys("count"), "normalize count")
|
|
return nil
|
|
}}, input, preparePostDecodeOverrideContext(input, decoded, assigned))
|
|
if len(resolveDiagnostics) != 0 {
|
|
t.Fatalf("resolveLoadInput() diagnostics = %#v", resolveDiagnostics)
|
|
}
|
|
if resolvedContext.metadata.historyBuilder == nil || resolvedContext.metadata.historyBuilder.model.resolutionPayload == nil ||
|
|
len(resolvedContext.metadata.historyBuilder.model.resolutionPayload.snapshots) != 1 {
|
|
t.Fatal("resolution evidence was not appended once to the handed-off model")
|
|
}
|
|
if location, ok := metadataFieldLocation(resolvedContext.metadata, Keys("count"), Origin{Layer: LayerResolution}); !ok || location.SourceID != countSource {
|
|
t.Fatalf("post-resolution historical location = %#v, %v", location, ok)
|
|
}
|
|
if info, ok := prepared.resolution.lookup(Keys("count")); !ok || !info.Explicit || info.Origin.Layer != LayerCLI {
|
|
t.Fatalf("resolution entry view changed after resolution: %#v, %v", info, ok)
|
|
}
|
|
if value, ok := documentAtPath(resolvedContext.referenceInput, Keys("count")); !ok || value.Location.SourceID != countSource {
|
|
t.Fatalf("post-resolution reference location = %#v, %v", value, ok)
|
|
}
|
|
|
|
freezeMetadataHistory(&prepared.metadata, assigned.final)
|
|
inspection := Inspection{
|
|
metadata: prepared.metadata, history: prepared.metadata.historyModel, final: assigned.final,
|
|
}
|
|
rateExplanation, ok := inspection.Explain(Keys("rate"))
|
|
if !ok {
|
|
t.Fatal("Explain(rate) did not return a history")
|
|
}
|
|
if len(rateExplanation.History) != 4 || rateExplanation.History[0].Origin.Layer != LayerDefault ||
|
|
rateExplanation.History[1].Origin.Layer != LayerMain || rateExplanation.History[2].Origin.Operation != ChangeDecoding ||
|
|
rateExplanation.History[3].Origin.Layer != LayerCLI {
|
|
t.Fatalf("rate history order = %#v", rateExplanation.History)
|
|
}
|
|
|
|
configuration := assigned.value.Interface().(config)
|
|
resolver := func(value *config, resolution *Resolution) error {
|
|
if info, found := resolution.InputInfo(Keys("count")); !found || !info.Explicit || info.Origin.Layer != LayerCLI {
|
|
t.Errorf("resolver input info = %#v, %v", info, found)
|
|
}
|
|
if source, found := resolution.Source(cliSource); !found || source.Layer != LayerCLI {
|
|
t.Errorf("resolver CLI source = %#v, %v", source, found)
|
|
}
|
|
Default(resolution, Keys("count"), &value.Count, 99, "fill missing count")
|
|
Default(resolution, Keys("derived"), &value.Derived, 42, "derive default")
|
|
value.APIKey = SecretEnv("RESOLVED_SECRET")
|
|
resolution.RecordNormalization(Keys("api_key"), "select effective secret reference")
|
|
return nil
|
|
}
|
|
resolved := runResolution(&configuration, resolver, schema, prepared.decoded, prepared.referenceInput, prepared.resolution)
|
|
if len(resolved.diagnostics) != 0 || configuration.Count != 5 || configuration.Derived != 42 {
|
|
t.Fatalf("resolution = %#v, configuration = %#v", resolved, configuration)
|
|
}
|
|
applyResolutionMetadata(&prepared.metadata, resolved, prepared.resolution)
|
|
effectiveReferenceInput := documentWithMetadataLocations(resolved.final, prepared.metadata)
|
|
if node, ok := documentAtPath(effectiveReferenceInput, Keys("api_key")); !ok || node.Text != "RESOLVED_SECRET" || node.Location.SourceID != cliSource {
|
|
t.Fatalf("effective reference/location = %#v", node)
|
|
}
|
|
missing := resolveSecretReferences(reflect.ValueOf(&configuration).Elem(), schema, effectiveReferenceInput, func(name string) (string, bool) {
|
|
if name != "RESOLVED_SECRET" {
|
|
t.Errorf("secret lookup name = %q", name)
|
|
}
|
|
return "", false
|
|
})
|
|
if len(missing.diagnostics) != 1 || missing.diagnostics[0].Code != CodeSecretUnset || missing.diagnostics[0].Primary == nil || missing.diagnostics[0].Primary.SourceID != cliSource {
|
|
t.Fatalf("secret failure handoff = %#v", missing.diagnostics)
|
|
}
|
|
}
|
|
|
|
func setOverrideTestDocumentSource(node *document.Node, sourceID string) {
|
|
if node == nil {
|
|
return
|
|
}
|
|
node.Location.SourceID = sourceID
|
|
for index := range node.Mapping {
|
|
node.Mapping[index].KeyLocation.SourceID = sourceID
|
|
node.Mapping[index].ValueLocation.SourceID = sourceID
|
|
setOverrideTestDocumentSource(node.Mapping[index].Value, sourceID)
|
|
}
|
|
for _, child := range node.Sequence {
|
|
setOverrideTestDocumentSource(child, sourceID)
|
|
}
|
|
}
|