Add configuration path explanations
This commit is contained in:
@@ -27,6 +27,153 @@ type capturedSourceReader struct {
|
||||
calls int
|
||||
}
|
||||
|
||||
func TestConfigExplainPreservesConfigKitProvenanceAndRedaction(t *testing.T) {
|
||||
type entry struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Credential configkit.Secret `yaml:"credential"`
|
||||
}
|
||||
type configuration struct {
|
||||
Entries map[string]entry `yaml:"entries"`
|
||||
Odd map[string]string `yaml:"odd"`
|
||||
Value string `yaml:"value"`
|
||||
Dependency string `yaml:"dependency"`
|
||||
Secret configkit.Secret `yaml:"secret"`
|
||||
}
|
||||
const secretValue = "EXPLAIN_TEST_SECRET_VALUE"
|
||||
t.Setenv("EXPLAIN_TEST_SECRET", secretValue)
|
||||
input := []byte("entries:\n parent:\n enabled: false\n credential: EXPLAIN_TEST_SECRET\n child:\n extends: parent\nodd:\n \"key/name~\": escaped\nvalue: original\ndependency: source\nsecret: EXPLAIN_TEST_SECRET\n")
|
||||
result, err := configkit.Load[configuration](configkit.Bytes("explanation.yml", input), configkit.Options[configuration]{Definition: configkit.Definition[configuration]{
|
||||
Secret: func(path configkit.Path) bool { return path.String() == "/entries/parent/credential" },
|
||||
Resolve: func(value *configuration, resolution *configkit.Resolution) error {
|
||||
resolution.Record(configkit.Keys("value"), configkit.ChangeNormalization, "upper case", configkit.Keys("dependency"))
|
||||
value.Value = strings.ToUpper(value.Value)
|
||||
return nil
|
||||
},
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
loads, contexts := 0, 0
|
||||
scope, err := commandkit.NewConfigKitScope[struct{}, configuration]("app", "", func(context.Context, commandkit.Selection, struct{}) (*configkit.Result[configuration], error) {
|
||||
loads++
|
||||
return result, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
app, err := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{NewContext: func() struct{} { contexts++; return struct{}{} }, Scopes: []commandkit.ConfigScope[struct{}]{scope}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runYAML := func(pointer string) (map[string]any, commandkit.Outcome, string) {
|
||||
var output bytes.Buffer
|
||||
argv := []string{"config", "explain", "--path", pointer, "--format", "yaml"}
|
||||
if pointer == "" {
|
||||
argv = []string{"config", "explain", "--path=", "--format", "yaml"}
|
||||
}
|
||||
outcome := app.Execute(context.Background(), argv, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||
var report map[string]any
|
||||
if outcome.Kind == commandkit.OutcomeSuccess {
|
||||
if err := yaml.Unmarshal(output.Bytes(), &report); err != nil {
|
||||
t.Fatalf("decode explanation YAML: %v; output=%q", err, output.String())
|
||||
}
|
||||
}
|
||||
return report, outcome, output.String()
|
||||
}
|
||||
|
||||
path := "/entries/child/enabled"
|
||||
var native bytes.Buffer
|
||||
if err := result.WriteExplanation(&native, configkit.Keys("entries", "child", "enabled")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var textOutput bytes.Buffer
|
||||
outcome := app.Execute(context.Background(), []string{"config", "explain", "--path", path}, commandkit.Streams{Out: &textOutput, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || textOutput.String() != native.String() {
|
||||
t.Fatalf("native text outcome=%#v output=%q want=%q", outcome, textOutput.String(), native.String())
|
||||
}
|
||||
var inherited map[string]any
|
||||
inherited, outcome, _ = runYAML(path)
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || inherited["path"] != path {
|
||||
t.Fatalf("inherited explanation outcome=%#v report=%#v", outcome, inherited)
|
||||
}
|
||||
effective := inherited["effective"].(map[string]any)
|
||||
if effective["present"] != true || effective["yaml"] != "false\n" {
|
||||
t.Fatalf("inherited effective snapshot = %#v", effective)
|
||||
}
|
||||
origin := inherited["origin"].(map[string]any)
|
||||
steps := origin["inheritance"].([]any)
|
||||
step := steps[0].(map[string]any)
|
||||
if step["from"] != "/entries/parent/enabled" || step["to"] != path || step["reference"] != "/entries/child/extends" {
|
||||
t.Fatalf("inheritance paths = %#v", step)
|
||||
}
|
||||
|
||||
escaped, outcome, _ := runYAML("/odd/key~1name~0")
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || escaped["path"] != "/odd/key~1name~0" || escaped["effective"].(map[string]any)["yaml"] != "escaped\n" {
|
||||
t.Fatalf("escaped path outcome=%#v report=%#v", outcome, escaped)
|
||||
}
|
||||
historical, outcome, _ := runYAML("/entries/child/extends")
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || historical["effective"].(map[string]any)["present"] != false || len(historical["history"].([]any)) == 0 {
|
||||
t.Fatalf("historical path outcome=%#v report=%#v", outcome, historical)
|
||||
}
|
||||
resolved, outcome, _ := runYAML("/value")
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || resolved["effective"].(map[string]any)["yaml"] != "ORIGINAL\n" {
|
||||
t.Fatalf("resolved snapshot outcome=%#v report=%#v", outcome, resolved)
|
||||
}
|
||||
changes := resolved["history"].([]any)
|
||||
last := changes[len(changes)-1].(map[string]any)
|
||||
if last["reasons"].([]any)[0] != "upper case" || last["dependencies"].([]any)[0] != "/dependency" {
|
||||
t.Fatalf("resolution change = %#v", last)
|
||||
}
|
||||
secretReport, outcome, secretText := runYAML("/entries/child/credential")
|
||||
secretEffective := secretReport["effective"].(map[string]any)
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || secretEffective["redacted"] != true || strings.Contains(secretText, secretValue) {
|
||||
t.Fatalf("secret report outcome=%#v report=%#v", outcome, secretReport)
|
||||
}
|
||||
for _, change := range secretReport["history"].([]any) {
|
||||
entry := change.(map[string]any)
|
||||
for _, key := range []string{"before", "after"} {
|
||||
snapshot := entry[key].(map[string]any)
|
||||
if snapshot["present"] == true && snapshot["redacted"] != true {
|
||||
t.Fatalf("secret history was not redacted: %#v", entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
directSecret, outcome, directSecretText := runYAML("/secret")
|
||||
secretRef, refPresent := directSecret["secret_reference"].(map[string]any)
|
||||
if !refPresent {
|
||||
t.Fatalf("secret reference was absent: %#v", directSecret)
|
||||
}
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || secretRef["name"] != "EXPLAIN_TEST_SECRET" || strings.Contains(directSecretText, secretValue) {
|
||||
t.Fatalf("direct secret reference outcome=%#v report=%#v", outcome, directSecret)
|
||||
}
|
||||
root, outcome, _ := runYAML("")
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || root["path"] != "" {
|
||||
t.Fatalf("root path outcome=%#v report=%#v", outcome, root)
|
||||
}
|
||||
|
||||
loadsBeforeInvalid, contextsBeforeInvalid := loads, contexts
|
||||
outcome = app.Execute(context.Background(), []string{"config", "explain"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeUsage || loads != loadsBeforeInvalid || contexts != contextsBeforeInvalid {
|
||||
t.Fatalf("missing path outcome=%#v loads=%d contexts=%d", outcome, loads, contexts)
|
||||
}
|
||||
outcome = app.Execute(context.Background(), []string{"config", "explain", "--path", "/bad~2path"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeUsage || outcome.Status != 2 || loads != loadsBeforeInvalid || contexts != contextsBeforeInvalid {
|
||||
t.Fatalf("malformed path outcome=%#v loads=%d contexts=%d", outcome, loads, contexts)
|
||||
}
|
||||
outcome = app.Execute(context.Background(), []string{"config", "explain", "--path", "/does-not-exist"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeOperational || outcome.Status != 1 {
|
||||
t.Fatalf("unknown path outcome = %#v", outcome)
|
||||
}
|
||||
partial := &configKitShowPartialWriter{failure: io.ErrClosedPipe}
|
||||
outcome = app.Execute(context.Background(), []string{"config", "explain", "--path", path}, commandkit.Streams{Out: partial, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeOperational || !outcome.ReportIncomplete || !errors.Is(outcome.DeliveryError, io.ErrClosedPipe) {
|
||||
t.Fatalf("explanation writer failure = %#v", outcome)
|
||||
}
|
||||
}
|
||||
|
||||
func (reader *capturedSourceReader) Sources() []configkit.SourceInfo {
|
||||
reader.calls++
|
||||
return reader.entries
|
||||
|
||||
Reference in New Issue
Block a user