package engine import ( "reflect" "testing" "gitea.maximumdirect.net/eric/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: LayerBase, 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.structuralInput == structural || prepared.preCLI == decoded.final || prepared.decoded.final == assigned.final || prepared.referenceInput == assigned.final { t.Fatal("structural, pre-CLI, post-CLI, and located snapshots are not detached") } if raw, ok := documentAtPath(prepared.structuralInput, Keys("version")); !ok || raw.Kind != document.Number || raw.Text != "1" { t.Fatalf("structural version = %#v", raw) } if typed, ok := documentAtPath(prepared.preCLI, Keys("version")); !ok || typed.Kind != document.String || typed.Text != "1" { t.Fatalf("pre-CLI typed version = %#v", typed) } 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 len(prepared.applications) != 1 || len(prepared.postDecodeApps) != len(batch.overrides) || len(prepared.historyApplications()) != 1+len(batch.overrides) { t.Fatalf("history applications = structural:%d CLI:%d all:%d", len(prepared.applications), len(prepared.postDecodeApps), len(prepared.historyApplications())) } if len(prepared.sources) != len(sources)+len(batch.overrides) || prepared.sources[len(prepared.sources)-1].Layer != LayerCLI { t.Fatalf("prepared source order = %#v", prepared.sources) } 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) } 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) } inspection := Inspection{ metadata: prepared.metadata, baseline: decoded.baseline, input: assigned.final, inputOrigins: assigned.origins, preCLI: prepared.preCLI, preCLIOrigins: prepared.preCLIOrigins, postDecodeApps: prepared.postDecodeApps, final: assigned.final, applications: prepared.applications, immutableLookup: buildDocumentLookup(prepared.preCLI, assigned.final, prepared.applications[0].value, prepared.postDecodeApps[0].value, prepared.postDecodeApps[1].value, prepared.postDecodeApps[2].value, prepared.postDecodeApps[3].value), } 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 != LayerBase || 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.Record(Keys("api_key"), ChangeNormalization, "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) 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) } }