517 lines
20 KiB
Go
517 lines
20 KiB
Go
package configkit
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|
)
|
|
|
|
type explanationConcurrency struct {
|
|
Transcription struct {
|
|
Concurrency int `yaml:"concurrency"`
|
|
} `yaml:"transcription"`
|
|
}
|
|
|
|
func TestExplainShowsDefaultBaseAndEffectiveProfileValue(t *testing.T) {
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "config.yml", "configkit:\n imports: [{path: transcription.yml}]\n")
|
|
writeImportFile(t, directory, "transcription.yml", "transcription:\n concurrency: 8\n")
|
|
writeImportFile(t, directory, filepath.Join("profiles", "testing.yml"), "transcription:\n concurrency: 1\n")
|
|
result, err := Load[explanationConcurrency](File(mainPath), Options[explanationConcurrency]{
|
|
Imports: true, Profile: "testing",
|
|
Definition: Definition[explanationConcurrency]{Defaults: func() explanationConcurrency {
|
|
value := explanationConcurrency{}
|
|
value.Transcription.Concurrency = 4
|
|
return value
|
|
}},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
explanation, ok := result.Explain(Keys("transcription", "concurrency"))
|
|
if !ok {
|
|
t.Fatal("known path was not explained")
|
|
}
|
|
if got := explanationValues(explanation); !reflect.DeepEqual(got, []string{"4\n", "8\n", "1\n"}) {
|
|
t.Fatalf("history values = %#v", got)
|
|
}
|
|
if explanation.Effective.YAML != "1\n" || explanation.Origin.Layer != LayerProfile ||
|
|
len(explanation.Origin.Locations) != 1 || explanation.Origin.Locations[0].SourceID != "source-3" ||
|
|
explanation.Origin.Locations[0].Line != 2 {
|
|
t.Fatalf("effective explanation = %#v", explanation)
|
|
}
|
|
}
|
|
|
|
type explanationConfig struct {
|
|
Concurrency int `yaml:"concurrency"`
|
|
Equal string `yaml:"equal"`
|
|
Limit int `yaml:"limit"`
|
|
Items []string `yaml:"items"`
|
|
Labels map[string]string `yaml:"labels"`
|
|
Dynamic any `yaml:"dynamic"`
|
|
Maybe *string `yaml:"maybe"`
|
|
Empty []string `yaml:"empty"`
|
|
Credential Secret `yaml:"credential"`
|
|
Classified Secret `yaml:"classified"`
|
|
DerivedA string `yaml:"derived_a"`
|
|
DerivedB string `yaml:"derived_b"`
|
|
}
|
|
|
|
func TestExplainReplaysCLIResolutionContainersAndSafeHistoricalValues(t *testing.T) {
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "config.yml", `concurrency: 8
|
|
equal: same
|
|
limit: wrong-before-profile
|
|
items: [base, removed]
|
|
labels: {base: retained}
|
|
dynamic: {retired: HISTORICAL_EXPLAIN_CANARY, public: visible}
|
|
credential: APP_SECRET
|
|
classified: APP_SECRET
|
|
`)
|
|
writeImportFile(t, directory, filepath.Join("profiles", "testing.yml"), `concurrency: 1
|
|
equal: same
|
|
limit: 7
|
|
items: [profile]
|
|
labels: {profile: retained}
|
|
dynamic: selected
|
|
`)
|
|
result, err := loadWithSecretLookup(File(mainPath), func(name string) (string, bool) {
|
|
if name != "APP_SECRET" {
|
|
t.Fatalf("secret lookup = %q", name)
|
|
}
|
|
return "RESOLVED_EXPLAIN_CANARY", true
|
|
}, Options[explanationConfig]{
|
|
Profile: "testing",
|
|
Overrides: []Override{
|
|
{Path: Keys("concurrency"), Value: 2, Flag: "--concurrency"},
|
|
{Path: Keys("equal"), Value: "same", Flag: "--equal"},
|
|
{Path: Keys("items"), Value: []any{"cli", "restored"}, Flag: "--items"},
|
|
{Path: Keys("labels"), Value: map[string]any{"cli": "retained"}, Flag: "--label"},
|
|
},
|
|
Definition: Definition[explanationConfig]{
|
|
Defaults: func() explanationConfig {
|
|
return explanationConfig{
|
|
Concurrency: 4, Equal: "same", Labels: map[string]string{"default": "retained"},
|
|
Maybe: nil, Empty: []string{}, Credential: SecretEnv("APP_SECRET"), Classified: SecretEnv("APP_SECRET"),
|
|
}
|
|
},
|
|
Secret: func(path Path) bool {
|
|
return path.String() == "/dynamic/retired" || path.String() == "/classified"
|
|
},
|
|
Resolve: func(configuration *explanationConfig, resolution *Resolution) error {
|
|
Normalize(resolution, Keys("concurrency"), &configuration.Concurrency, 3,
|
|
"normalized\nconcurrency", Keys("labels"))
|
|
Normalize(resolution, Keys("equal"), &configuration.Equal, "same", "record equal value")
|
|
Normalize(resolution, Keys("derived_a"), &configuration.DerivedA, "DERIVED_EXPLAIN_A",
|
|
"derived cycle a", Keys("derived_b"))
|
|
Normalize(resolution, Keys("derived_b"), &configuration.DerivedB, "DERIVED_EXPLAIN_B",
|
|
"derived cycle b", Keys("derived_a"), Keys("dynamic"))
|
|
return nil
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
concurrency, ok := result.Explain(Keys("concurrency"))
|
|
if !ok {
|
|
t.Fatal("concurrency was not explained")
|
|
}
|
|
if got := explanationValues(concurrency); !reflect.DeepEqual(got, []string{"4\n", "8\n", "1\n", "2\n", "3\n"}) {
|
|
t.Fatalf("concurrency history = %#v", got)
|
|
}
|
|
if got := historyValueLayers(concurrency); !reflect.DeepEqual(got, []Layer{LayerDefault, LayerBase, LayerProfile, LayerCLI, LayerResolution}) {
|
|
t.Fatalf("concurrency layers = %#v", got)
|
|
}
|
|
if concurrency.History[4].Before.YAML != "2\n" || concurrency.History[4].Reasons[0] != "normalized\nconcurrency" ||
|
|
concurrency.History[4].Dependencies[0].String() != "/labels" {
|
|
t.Fatalf("resolution history = %#v", concurrency.History[4])
|
|
}
|
|
|
|
equal, _ := result.Explain(Keys("equal"))
|
|
if len(equal.History) != 5 {
|
|
t.Fatalf("equal writes were lost: %#v", equal.History)
|
|
}
|
|
for _, change := range equal.History[1:] {
|
|
if change.Before.YAML != "same\n" || change.After.YAML != "same\n" {
|
|
t.Fatalf("equal replacement = %#v", change)
|
|
}
|
|
}
|
|
|
|
limit, _ := result.Explain(Keys("limit"))
|
|
if got := explanationValues(limit); !reflect.DeepEqual(got, []string{"0\n", "wrong-before-profile\n", "7\n"}) {
|
|
t.Fatalf("overridden invalid input was not retained: %#v", got)
|
|
}
|
|
removed, ok := result.Explain(Keys("dynamic", "retired"))
|
|
if !ok || removed.Effective.Present || len(removed.History) < 2 {
|
|
t.Fatalf("removed path = %#v, %v", removed, ok)
|
|
}
|
|
for _, change := range removed.History {
|
|
if strings.Contains(change.Before.YAML+change.After.YAML, "HISTORICAL_EXPLAIN_CANARY") {
|
|
t.Fatalf("historical secret escaped redaction: %#v", change)
|
|
}
|
|
}
|
|
if !removed.History[0].After.Redacted || removed.History[len(removed.History)-1].After.Present {
|
|
t.Fatalf("removed secret history = %#v", removed.History)
|
|
}
|
|
dynamic, _ := result.Explain(Keys("dynamic"))
|
|
if len(dynamic.History) < 3 || !dynamic.History[1].After.Redacted ||
|
|
strings.Contains(dynamic.History[1].After.YAML, "HISTORICAL_EXPLAIN_CANARY") {
|
|
t.Fatalf("container redaction history = %#v", dynamic.History)
|
|
}
|
|
|
|
items, _ := result.Explain(Keys("items"))
|
|
if len(items.History) != 4 || !strings.Contains(items.History[1].After.YAML, "removed") ||
|
|
strings.Contains(items.History[2].After.YAML, "removed") || !strings.Contains(items.History[3].After.YAML, "restored") {
|
|
t.Fatalf("atomic sequence history = %#v", items.History)
|
|
}
|
|
removedIndex, ok := result.Explain(Keys("items").Index(1))
|
|
if !ok || !removedIndex.History[1].Before.Present || removedIndex.History[1].After.Present ||
|
|
!removedIndex.History[2].After.Present {
|
|
t.Fatalf("sequence descendant history = %#v", removedIndex.History)
|
|
}
|
|
labels, _ := result.Explain(Keys("labels"))
|
|
if len(labels.History) != 4 || !strings.Contains(labels.Effective.YAML, "default: retained") ||
|
|
!strings.Contains(labels.Effective.YAML, "profile: retained") || !strings.Contains(labels.Effective.YAML, "cli: retained") {
|
|
t.Fatalf("composite mapping history = %#v", labels)
|
|
}
|
|
if labels.Origin.Layer != LayerCLI || len(labels.Origin.Locations) != 3 {
|
|
t.Fatalf("composite effective origin = %#v", labels.Origin)
|
|
}
|
|
|
|
maybe, _ := result.Explain(Keys("maybe"))
|
|
empty, _ := result.Explain(Keys("empty"))
|
|
if !maybe.Effective.Present || maybe.Effective.YAML != "null\n" || !empty.Effective.Present || empty.Effective.YAML != "[]\n" {
|
|
t.Fatalf("null/empty snapshots = maybe %#v empty %#v", maybe.Effective, empty.Effective)
|
|
}
|
|
credential, _ := result.Explain(Keys("credential"))
|
|
if credential.SecretReference == nil || credential.SecretReference.Name != "APP_SECRET" || credential.Effective.YAML != "APP_SECRET\n" {
|
|
t.Fatalf("secret reference = %#v", credential)
|
|
}
|
|
classified, _ := result.Explain(Keys("classified"))
|
|
if classified.SecretReference != nil || !classified.Effective.Redacted ||
|
|
strings.Contains(classified.Effective.YAML, "APP_SECRET") {
|
|
t.Fatalf("classified secret reference = %#v", classified)
|
|
}
|
|
var derivedExplanations []Explanation
|
|
for _, path := range []Path{Keys("derived_a"), Keys("derived_b")} {
|
|
derived, _ := result.Explain(path)
|
|
if !derived.Effective.Redacted || strings.Contains(derived.Effective.YAML, "DERIVED_EXPLAIN") {
|
|
t.Fatalf("dependency closure at %s = %#v", path.String(), derived)
|
|
}
|
|
derivedExplanations = append(derivedExplanations, derived)
|
|
}
|
|
|
|
formatted := fmt.Sprintf("%v %#v %v", removed, credential, derivedExplanations)
|
|
encoded, marshalErr := json.Marshal(append([]Explanation{removed, credential, classified}, derivedExplanations...))
|
|
if marshalErr != nil || strings.Contains(formatted+string(encoded), "RESOLVED_EXPLAIN_CANARY") ||
|
|
strings.Contains(formatted+string(encoded), "HISTORICAL_EXPLAIN_CANARY") ||
|
|
strings.Contains(formatted+string(encoded), "DERIVED_EXPLAIN") {
|
|
t.Fatalf("public records disclosed a canary: %s %s", formatted, encoded)
|
|
}
|
|
|
|
concurrency.Path.segments[0].key = "mutated"
|
|
concurrency.History[4].Reasons[0] = "mutated"
|
|
concurrency.History[4].Dependencies[0].segments[0].key = "mutated"
|
|
result.Config.Concurrency = 99
|
|
again, _ := result.Explain(Keys("concurrency"))
|
|
if again.Path.String() != "/concurrency" || again.Effective.YAML != "3\n" ||
|
|
again.History[4].Reasons[0] != "normalized\nconcurrency" ||
|
|
again.History[4].Dependencies[0].String() != "/labels" {
|
|
t.Fatalf("returned explanation aliased internal history: %#v", again)
|
|
}
|
|
|
|
var first, second bytes.Buffer
|
|
if err := result.WriteExplanation(&first, Keys("concurrency")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := result.WriteExplanation(&second, Keys("concurrency")); err != nil || first.String() != second.String() {
|
|
t.Fatalf("nondeterministic rendering: %v\n%s\n%s", err, first.String(), second.String())
|
|
}
|
|
if strings.Contains(first.String(), "\nconcurrency\"") || !strings.Contains(first.String(), `"normalized\nconcurrency"`) {
|
|
t.Fatalf("reason controls were not escaped: %q", first.String())
|
|
}
|
|
unknownOutput := bytes.NewBufferString("unchanged")
|
|
if err := result.WriteExplanation(unknownOutput, Keys("unknown")); !errors.Is(err, errExplanationUnknownPath) || unknownOutput.String() != "unchanged" {
|
|
t.Fatalf("unknown writer = %q, %v", unknownOutput.String(), err)
|
|
}
|
|
if _, ok := result.Explain(Keys("items").Index(-1)); ok {
|
|
t.Fatal("malformed path was explained")
|
|
}
|
|
wantWriteError := errors.New("write failed")
|
|
if err := result.WriteExplanation(errorWriter{err: wantWriteError}, Keys("concurrency")); !errors.Is(err, wantWriteError) {
|
|
t.Fatalf("writer error = %v", err)
|
|
}
|
|
if err := result.WriteExplanation(shortWriter{}, Keys("concurrency")); !errors.Is(err, io.ErrShortWrite) {
|
|
t.Fatalf("short writer error = %v", err)
|
|
}
|
|
var invalidInput bytes.Buffer
|
|
if err := result.WriteExplanation(&invalidInput, Keys("limit")); err != nil ||
|
|
!strings.Contains(invalidInput.String(), "policy-validated input") ||
|
|
strings.Contains(invalidInput.String(), "wrong-before-profile\n source: layer=\"base\"\n before (canonical typed)") {
|
|
t.Fatalf("historical representation rendering = %v\n%s", err, invalidInput.String())
|
|
}
|
|
}
|
|
|
|
func TestSecretsBootstrapLoadAndExplanationRemainSeparateAndSafe(t *testing.T) {
|
|
const name = "CONFIGKIT_EXPLAIN_BOOTSTRAP_TEST"
|
|
previous, present := os.LookupEnv(name)
|
|
if err := os.Unsetenv(name); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if present {
|
|
_ = os.Setenv(name, previous)
|
|
} else {
|
|
_ = os.Unsetenv(name)
|
|
}
|
|
})
|
|
directory := t.TempDir()
|
|
writeSecretTestFile(t, directory, name, "BOOTSTRAP_EXPLAIN_CANARY\n")
|
|
report, err := LoadSecretsDir(directory)
|
|
if err != nil || len(report.Entries()) != 1 || report.Entries()[0].Status != SecretFileLoaded {
|
|
t.Fatalf("bootstrap = %#v, %v", report, err)
|
|
}
|
|
type config struct {
|
|
Credential Secret `yaml:"credential"`
|
|
}
|
|
result, err := Load[config](Bytes("config.yml", []byte("credential: "+name+"\n")))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var output bytes.Buffer
|
|
if err := result.WriteExplanation(&output, Keys("credential")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(output.String(), name) || strings.Contains(output.String(), "BOOTSTRAP_EXPLAIN_CANARY") {
|
|
t.Fatalf("explanation mixed bootstrap value into reference report: %s", output.String())
|
|
}
|
|
}
|
|
|
|
func TestHistoryNodeBudgetCountsSharedTreesOnce(t *testing.T) {
|
|
leaf := &document.Node{Kind: document.String, Text: "value"}
|
|
root := &document.Node{Kind: document.Mapping, Mapping: []document.Entry{{Key: "value", Value: leaf}}}
|
|
applications := []historyApplication{{layer: LayerBase, value: root}}
|
|
if !historyNodesWithin(2, root, root, root, applications, nil) {
|
|
t.Fatal("structurally shared history was counted more than once")
|
|
}
|
|
other := document.Clone(root)
|
|
if historyNodesWithin(3, root, other, root, applications, nil) {
|
|
t.Fatal("distinct retained nodes did not consume the budget")
|
|
}
|
|
}
|
|
|
|
func explanationValues(explanation Explanation) []string {
|
|
values := make([]string, len(explanation.History))
|
|
for index, change := range explanation.History {
|
|
values[index] = change.After.YAML
|
|
}
|
|
return values
|
|
}
|
|
|
|
func historyValueLayers(explanation Explanation) []Layer {
|
|
layers := make([]Layer, len(explanation.History))
|
|
for index, change := range explanation.History {
|
|
layers[index] = change.Origin.Layer
|
|
}
|
|
return layers
|
|
}
|
|
|
|
func TestExplanationReconcilesDefaultsAfterInputReplacement(t *testing.T) {
|
|
type cfg struct {
|
|
M map[string]int `yaml:"m"`
|
|
}
|
|
for _, base := range []string{"null", "replaced-text"} {
|
|
for _, cli := range []bool{false, true} {
|
|
t.Run(fmt.Sprintf("%s/cli=%v", base, cli), func(t *testing.T) {
|
|
root := t.TempDir()
|
|
main := writeImportFile(t, root, "main.yml", "m: "+base+"\n")
|
|
opts := Options[cfg]{Definition: Definition[cfg]{Defaults: func() cfg { return cfg{M: map[string]int{"a": 1}} }}}
|
|
if cli {
|
|
opts.Overrides = []Override{{Path: Keys("m"), Value: map[string]any{"b": 2}, Flag: "--m"}}
|
|
} else {
|
|
writeImportFile(t, root, "profiles/p.yml", "m: {b: 2}\n")
|
|
opts.Profile = "p"
|
|
}
|
|
result, err := Load(File(main), opts)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, path := range []Path{Keys("m"), Keys("m", "a")} {
|
|
explanation, ok := result.Explain(path)
|
|
if !ok {
|
|
t.Fatal("missing explanation")
|
|
}
|
|
last := explanation.History[len(explanation.History)-1]
|
|
if last.After != explanation.Effective || last.Origin.Operation != ChangeDecoding {
|
|
t.Fatalf("history does not reconcile final value: %#v", explanation)
|
|
}
|
|
info, _ := result.Metadata().Lookup(path)
|
|
if last.Origin.Layer != info.Origin.Layer {
|
|
t.Fatal("decoding lost effective origin")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIntegerVersionInspectionRetainsInputAndReconcilesTypedValue(t *testing.T) {
|
|
type config struct {
|
|
Version string `yaml:"version"`
|
|
}
|
|
rule := &VersionRule{Path: Keys("version"), Check: func(version Version, _ *Validation) error {
|
|
if version.Text != "4" {
|
|
t.Errorf("version = %#v", version)
|
|
}
|
|
return nil
|
|
}}
|
|
options := Options[config]{Definition: Definition[config]{
|
|
Defaults: func() config { return config{Version: "4"} },
|
|
Version: rule,
|
|
}}
|
|
first, err := Load(Bytes("version.yml", []byte("version: 4\n")), options)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := Load(Bytes("version.yml", []byte("version: 4\n")), options)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if first.Config.Version != "4" || second.Config.Version != "4" {
|
|
t.Fatalf("configurations = %#v / %#v", first.Config, second.Config)
|
|
}
|
|
inputVersion, inputOK := documentAtPath(first.applications[0].value, Keys("version"))
|
|
finalVersion, finalOK := documentAtPath(first.final, Keys("version"))
|
|
if !inputOK || inputVersion.Kind != document.Number || inputVersion.Text != "4" ||
|
|
!finalOK || finalVersion.Kind != document.String || finalVersion.Text != "4" {
|
|
t.Fatalf("input/final = %#v / %#v", inputVersion, finalVersion)
|
|
}
|
|
|
|
info, ok := first.Metadata().Lookup(Keys("version"))
|
|
if !ok || !info.Explicit || info.Null || info.Origin.Layer != LayerBase || len(info.Origin.Locations) != 1 ||
|
|
info.Origin.Locations[0].SourceID != "source-1" || info.Origin.Locations[0].Line != 1 {
|
|
t.Fatalf("version metadata = %#v, %v", info, ok)
|
|
}
|
|
explanation, ok := first.Explain(Keys("version"))
|
|
if !ok || explanation.Effective.YAML != "'4'\n" {
|
|
t.Fatalf("version explanation = %#v, %v", explanation, ok)
|
|
}
|
|
foundInput, foundDecoding := false, false
|
|
for _, change := range explanation.History {
|
|
if change.After.YAML == "4\n" && change.Origin.Layer == LayerBase {
|
|
foundInput = true
|
|
}
|
|
if change.Before.YAML == "4\n" && change.After.YAML == "'4'\n" && change.Origin.Operation == ChangeDecoding {
|
|
foundDecoding = true
|
|
}
|
|
if change.Origin.Layer == LayerResolution {
|
|
t.Fatalf("conversion recorded as resolution: %#v", change)
|
|
}
|
|
}
|
|
if !foundInput || !foundDecoding {
|
|
t.Fatalf("version history = %#v", explanation.History)
|
|
}
|
|
paths := first.Metadata().HistoricalPaths()
|
|
foundPath := false
|
|
for _, path := range paths {
|
|
if path.String() == "/version" {
|
|
foundPath = true
|
|
}
|
|
}
|
|
if !foundPath || len(first.Metadata().History(Keys("version"))) < 2 {
|
|
t.Fatalf("historical version paths/history = %#v / %#v", paths, first.Metadata().History(Keys("version")))
|
|
}
|
|
|
|
var effective strings.Builder
|
|
if err := first.WriteEffective(&effective); err != nil || effective.String() != "version: '4'\n" {
|
|
t.Fatalf("effective output = %q, %v", effective.String(), err)
|
|
}
|
|
first.Config.Version = "mutated"
|
|
effective.Reset()
|
|
if err := first.WriteEffective(&effective); err != nil || effective.String() != "version: '4'\n" || second.Config.Version != "4" {
|
|
t.Fatalf("detached results = %q / %#v, %v", effective.String(), second.Config, err)
|
|
}
|
|
}
|
|
|
|
func TestSensitiveIntegerVersionStaysRedactedWithoutSecretLookup(t *testing.T) {
|
|
type config struct {
|
|
Version string `yaml:"version"`
|
|
}
|
|
lookups := 0
|
|
result, err := loadWithSecretLookup(Bytes("version.yml", []byte("version: 987654321\n")), func(string) (string, bool) {
|
|
lookups++
|
|
return "CREDENTIAL_CANARY", true
|
|
}, Options[config]{Definition: Definition[config]{
|
|
Version: &VersionRule{Path: Keys("version"), Check: func(Version, *Validation) error { return nil }},
|
|
Secret: func(path Path) bool { return path.String() == "/version" },
|
|
}})
|
|
if err != nil || result.Config.Version != "987654321" || lookups != 0 {
|
|
t.Fatalf("result = %#v, lookups = %d, err = %v", result, lookups, err)
|
|
}
|
|
var effective strings.Builder
|
|
if err := result.WriteEffective(&effective); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
explanation, ok := result.Explain(Keys("version"))
|
|
formatted := effective.String() + fmt.Sprintf("%#v %#v", explanation, result.Metadata().History(Keys("version")))
|
|
if !ok || !explanation.Effective.Redacted || strings.Contains(formatted, "987654321") || strings.Contains(formatted, "CREDENTIAL_CANARY") {
|
|
t.Fatalf("sensitive inspection = %q / %#v", effective.String(), explanation)
|
|
}
|
|
}
|
|
|
|
func TestExplanationReplaysResolutionAdditionsAndOverlappingIntent(t *testing.T) {
|
|
type cfg struct {
|
|
M map[string]any `yaml:"m"`
|
|
}
|
|
for _, overlap := range []bool{false, true} {
|
|
t.Run(fmt.Sprint(overlap), func(t *testing.T) {
|
|
result, err := Load(Bytes("base", []byte("m: {old: 1}\n")), Options[cfg]{Definition: Definition[cfg]{Resolve: func(c *cfg, r *Resolution) error {
|
|
c.M["new"] = map[string]any{"child": int64(7)}
|
|
delete(c.M, "old")
|
|
r.Record(Keys("m", "new"), ChangeDerivedDefault, "add nested", Keys("m", "old"))
|
|
r.Record(Keys("m", "old"), ChangeNormalization, "remove old")
|
|
if overlap {
|
|
r.Record(Keys("m"), ChangeNormalization, "update container")
|
|
}
|
|
return nil
|
|
}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, path := range []Path{Keys("m"), Keys("m", "new"), Keys("m", "new", "child"), Keys("m", "old")} {
|
|
x, ok := result.Explain(path)
|
|
if !ok {
|
|
t.Fatal("missing known history")
|
|
}
|
|
var count int
|
|
for _, v := range x.History {
|
|
if v.Origin.Layer == LayerResolution {
|
|
count++
|
|
if overlap && len(v.Reasons) != 3 {
|
|
t.Fatal("lost overlapping intent")
|
|
}
|
|
}
|
|
}
|
|
if overlap && count != 1 {
|
|
t.Fatalf("overlapping records applied %d times", count)
|
|
}
|
|
last := x.History[len(x.History)-1]
|
|
if last.After != x.Effective {
|
|
t.Fatalf("incorrect replay for %s: %#v", path, x)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|