328 lines
12 KiB
Go
328 lines
12 KiB
Go
package configkit_test
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
|
)
|
|
|
|
type facadeDiffConfig struct {
|
|
Value string `yaml:"value"`
|
|
Count int `yaml:"count"`
|
|
Items []string `yaml:"items"`
|
|
Dynamic map[string]any `yaml:"dynamic"`
|
|
}
|
|
|
|
type facadeDiffSecretConfig struct {
|
|
Secret configkit.Secret `yaml:"secret"`
|
|
}
|
|
|
|
func TestFacadeDiffKeepsPublicSequenceChangesWithHistoricalDescendants(t *testing.T) {
|
|
type configuration struct {
|
|
Items []any `yaml:"items"`
|
|
}
|
|
load := func(public string) *configkit.Result[configuration] {
|
|
t.Helper()
|
|
result, err := configkit.Load(configkit.Bytes("historical-sequence.yml", []byte("items: [{secret: disposable-canary}, "+public+"]\n")),
|
|
configkit.Options[configuration]{Definition: configkit.Definition[configuration]{
|
|
Secret: func(path configkit.Path) bool { return path.String() == "/items/0/secret" },
|
|
Resolve: func(current *configuration, resolution *configkit.Resolution) error {
|
|
current.Items[0] = "public scalar"
|
|
resolution.Record(configkit.Keys("items"), configkit.ChangeNormalization, "normalize first item")
|
|
return nil
|
|
},
|
|
}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return result
|
|
}
|
|
left, right := load("1"), load("2")
|
|
history, ok := left.Explain(configkit.Keys("items").Index(0).Key("secret"))
|
|
if !ok || history.Effective.Present || len(history.History) == 0 {
|
|
t.Fatalf("removed historical secret = %#v, known=%v", history, ok)
|
|
}
|
|
maskedHistory := false
|
|
for _, change := range history.History {
|
|
maskedHistory = maskedHistory || change.Before.Redacted || change.After.Redacted
|
|
}
|
|
if !maskedHistory {
|
|
t.Fatal("historical sensitivity was not retained")
|
|
}
|
|
differences, err := configkit.Diff(left, right)
|
|
if err != nil || len(differences) != 1 || differences[0].Path.String() != "/items" {
|
|
t.Fatalf("sequence difference = %#v, %v", differences, err)
|
|
}
|
|
difference := differences[0]
|
|
if !difference.Left.Present || !difference.Right.Present || difference.Left.Redacted || difference.Right.Redacted || difference.HiddenChanged {
|
|
t.Fatalf("public sequence change was concealed: %#v", difference)
|
|
}
|
|
if difference.Left.YAML != "- public scalar\n- 1\n" || difference.Right.YAML != "- public scalar\n- 2\n" {
|
|
t.Fatalf("public sequence snapshots = %#v", difference)
|
|
}
|
|
}
|
|
|
|
func TestFacadeDiffKeepsPublicScalarAcrossHistoricalAbsence(t *testing.T) {
|
|
type configuration struct {
|
|
Dynamic map[string]any `yaml:"dynamic"`
|
|
}
|
|
options := configkit.Options[configuration]{Definition: configkit.Definition[configuration]{
|
|
Secret: func(path configkit.Path) bool { return path.String() == "/dynamic/entry/token" },
|
|
Resolve: func(current *configuration, resolution *configkit.Resolution) error {
|
|
delete(current.Dynamic, "entry")
|
|
resolution.Record(configkit.Keys("dynamic", "entry"), configkit.ChangeNormalization, "remove entry")
|
|
return nil
|
|
},
|
|
}}
|
|
absent, err := configkit.Load(configkit.Bytes("removed-entry.yml", []byte("dynamic: {entry: {token: disposable-canary}}\n")), options)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
options.Definition.Resolve = nil
|
|
present, err := configkit.Load(configkit.Bytes("public-entry.yml", []byte("dynamic: {entry: public}\n")), options)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, test := range []struct {
|
|
name string
|
|
left, right *configkit.Result[configuration]
|
|
leftPresent bool
|
|
}{
|
|
{name: "addition", left: absent, right: present},
|
|
{name: "removal", left: present, right: absent, leftPresent: true},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
differences, err := configkit.Diff(test.left, test.right)
|
|
if err != nil || len(differences) != 1 || differences[0].Path.String() != "/dynamic/entry" {
|
|
t.Fatalf("entry difference = %#v, %v", differences, err)
|
|
}
|
|
difference := differences[0]
|
|
if difference.Left.Present != test.leftPresent || difference.Right.Present == test.leftPresent ||
|
|
difference.Left.Redacted || difference.Right.Redacted || difference.HiddenChanged {
|
|
t.Fatalf("public presence change was concealed: %#v", difference)
|
|
}
|
|
wantLeft, wantRight := "", "public\n"
|
|
if test.leftPresent {
|
|
wantLeft, wantRight = wantRight, wantLeft
|
|
}
|
|
if difference.Left.YAML != wantLeft || difference.Right.YAML != wantRight {
|
|
t.Fatalf("entry snapshots = %#v", difference)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFacadeDiffReturnsDetachedCanonicalRecords(t *testing.T) {
|
|
left, err := configkit.Load[facadeDiffConfig](configkit.Bytes("left.yml", []byte("value: before\ncount: 0\nitems: [one, two]\ndynamic: {}\n")))
|
|
if err != nil {
|
|
t.Fatalf("load left: %v", err)
|
|
}
|
|
right, err := configkit.Load[facadeDiffConfig](configkit.Bytes("right.yml", []byte("value: after\ncount: 0\nitems: [one, three]\ndynamic:\n added: null\n")))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
differences, err := configkit.Diff(left, right)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantPaths := []string{"/dynamic/added", "/items", "/value"}
|
|
if len(differences) != len(wantPaths) {
|
|
t.Fatalf("differences = %#v", differences)
|
|
}
|
|
for index, difference := range differences {
|
|
if got := difference.Path.String(); got != wantPaths[index] {
|
|
t.Errorf("path[%d] = %q, want %q", index, got, wantPaths[index])
|
|
}
|
|
}
|
|
if differences[0].Left.Present || !differences[0].Right.Present || differences[0].Right.YAML != "null\n" {
|
|
t.Errorf("null/absence snapshots = %#v / %#v", differences[0].Left, differences[0].Right)
|
|
}
|
|
if differences[1].Left.YAML != "- one\n- two\n" || differences[1].Right.YAML != "- one\n- three\n" {
|
|
t.Errorf("sequence snapshots = %#v / %#v", differences[1].Left, differences[1].Right)
|
|
}
|
|
|
|
left.Config.Value = "caller mutation"
|
|
left.Config.Items[0] = "caller mutation"
|
|
right.Config.Dynamic["added"] = "caller mutation"
|
|
differences[2].Left.YAML = "record mutation"
|
|
differences[2].Path = configkit.Keys("rewritten")
|
|
|
|
again, err := configkit.Diff(left, right)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if again[2].Path.String() != "/value" || again[2].Left.YAML != "before\n" || again[1].Left.YAML != "- one\n- two\n" {
|
|
t.Fatalf("repeat diff changed = %#v", again)
|
|
}
|
|
explanation, ok := left.Explain(configkit.Keys("value"))
|
|
if !ok || explanation.Effective.YAML != "before\n" {
|
|
t.Fatalf("frozen explanation = %#v, %v", explanation, ok)
|
|
}
|
|
}
|
|
|
|
func TestFacadeDiffRejectsUnavailableResults(t *testing.T) {
|
|
type config struct {
|
|
Value string `yaml:"value"`
|
|
}
|
|
loaded, err := configkit.Load[config](configkit.Bytes("valid.yml", []byte("value: valid\n")))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var nilResult *configkit.Result[config]
|
|
zeroResult := &configkit.Result[config]{Config: config{Value: "must not be inspected"}}
|
|
for name, left := range map[string]*configkit.Result[config]{"nil": nilResult, "zero": zeroResult} {
|
|
t.Run(name, func(t *testing.T) {
|
|
if records, err := configkit.Diff(left, loaded); err == nil || records != nil {
|
|
t.Fatalf("Diff = %#v, %v; want nil records and error", records, err)
|
|
}
|
|
})
|
|
}
|
|
if records, err := configkit.Diff(loaded, nilResult); err == nil || records != nil {
|
|
t.Fatalf("right nil Diff = %#v, %v", records, err)
|
|
}
|
|
}
|
|
|
|
func TestFacadeDiffIgnoresSourceAndResolutionHistory(t *testing.T) {
|
|
type config struct {
|
|
Count int `yaml:"count"`
|
|
}
|
|
defaults := configkit.Options[config]{Definition: configkit.Definition[config]{
|
|
Defaults: func() config { return config{Count: 7} },
|
|
}}
|
|
defaulted, err := configkit.Load[config](configkit.NoSource(), defaults)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
explicit, err := configkit.Load[config](configkit.Bytes("explicit.yml", []byte("count: 7\n")))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if differences, err := configkit.Diff(defaulted, explicit); err != nil || len(differences) != 0 {
|
|
t.Fatalf("default/source difference = %#v, %v", differences, err)
|
|
}
|
|
|
|
zeroDefault, err := configkit.Load[config](configkit.NoSource(), configkit.Options[config]{Definition: configkit.Definition[config]{Defaults: func() config { return config{} }}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
explicitZero, err := configkit.Load[config](configkit.Bytes("explicit-zero.yml", []byte("count: 0\n")))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if differences, err := configkit.Diff(zeroDefault, explicitZero); err != nil || len(differences) != 0 {
|
|
t.Fatalf("explicit/defaulted zero difference = %#v, %v", differences, err)
|
|
}
|
|
|
|
resolve := func(value string) configkit.Options[config] {
|
|
return configkit.Options[config]{Definition: configkit.Definition[config]{Resolve: func(current *config, resolution *configkit.Resolution) error {
|
|
current.Count = len(value) + 8
|
|
resolution.Record(configkit.Keys("count"), configkit.ChangeNormalization, "set effective count")
|
|
return nil
|
|
}}}
|
|
}
|
|
resolvedA, err := configkit.Load[config](configkit.Bytes("a.yml", []byte("{}\n")), resolve("a"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
resolvedB, err := configkit.Load[config](configkit.Bytes("b.yml", []byte("{}\n")), resolve("b"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if differences, err := configkit.Diff(resolvedA, resolvedB); err != nil || len(differences) != 0 {
|
|
t.Fatalf("equal resolved values = %#v, %v", differences, err)
|
|
}
|
|
}
|
|
|
|
func TestFacadeDiffUsesFrozenFilesAndRunsCallbacksOnlyDuringLoad(t *testing.T) {
|
|
type config struct {
|
|
Value string `yaml:"value"`
|
|
}
|
|
calls := 0
|
|
options := configkit.Options[config]{Definition: configkit.Definition[config]{Defaults: func() config {
|
|
calls++
|
|
return config{}
|
|
}}}
|
|
dir := t.TempDir()
|
|
paths := []string{filepath.Join(dir, "left.yml"), filepath.Join(dir, "right.yml")}
|
|
for index, contents := range []string{"value: left\n", "value: right\n"} {
|
|
if err := os.WriteFile(paths[index], []byte(contents), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
left, err := configkit.Load[config](configkit.File(paths[0]), options)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
right, err := configkit.Load[config](configkit.File(paths[1]), options)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if calls != 2 {
|
|
t.Fatalf("defaults calls after loads = %d", calls)
|
|
}
|
|
for _, path := range paths {
|
|
if err := os.Remove(path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
differences, err := configkit.Diff(left, right)
|
|
if err != nil || len(differences) != 1 || differences[0].Path.String() != "/value" {
|
|
t.Fatalf("Diff after removing files = %#v, %v", differences, err)
|
|
}
|
|
if calls != 2 {
|
|
t.Fatalf("defaults reran during Diff: %d calls", calls)
|
|
}
|
|
}
|
|
|
|
func TestFacadeDiffCombinesSecretSensitivityAndLoadErrors(t *testing.T) {
|
|
t.Setenv("FACADE_DIFF_SECRET", "credential-one")
|
|
secretOptions := configkit.Options[facadeDiffSecretConfig]{Definition: configkit.Definition[facadeDiffSecretConfig]{Secret: func(path configkit.Path) bool {
|
|
return path.String() == "/secret"
|
|
}}}
|
|
left, err := configkit.Load[facadeDiffSecretConfig](configkit.Bytes("left.yml", []byte("secret: FACADE_DIFF_SECRET\n")), secretOptions)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("FACADE_DIFF_SECRET", "credential-two")
|
|
right, err := configkit.Load[facadeDiffSecretConfig](configkit.Bytes("right.yml", []byte("secret: FACADE_DIFF_SECRET\n")), secretOptions)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if differences, err := configkit.Diff(left, right); err != nil || len(differences) != 0 {
|
|
t.Fatalf("credential rotation = %#v, %v", differences, err)
|
|
}
|
|
t.Setenv("OTHER_FACADE_DIFF_SECRET", "other-credential")
|
|
otherReference, err := configkit.Load[facadeDiffSecretConfig](configkit.Bytes("other.yml", []byte("secret: OTHER_FACADE_DIFF_SECRET\n")), secretOptions)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
differences, err := configkit.Diff(left, otherReference)
|
|
if err != nil || len(differences) != 1 || !differences[0].HiddenChanged || !differences[0].Left.Redacted || !differences[0].Right.Redacted {
|
|
t.Fatalf("reference change = %#v, %v", differences, err)
|
|
}
|
|
if strings.Contains(differences[0].Left.YAML+differences[0].Right.YAML, "credential") {
|
|
t.Fatalf("credential leaked: %#v", differences)
|
|
}
|
|
|
|
invalidLeft, leftErr := configkit.Load[facadeDiffSecretConfig](configkit.Bytes("invalid-left.yml", []byte("unknown: true\n")))
|
|
if invalidLeft != nil || leftErr == nil {
|
|
t.Fatalf("invalid left load = %#v, %v", invalidLeft, leftErr)
|
|
}
|
|
var loadError *configkit.LoadError
|
|
if !errors.As(leftErr, &loadError) {
|
|
t.Fatalf("left load error = %T, want *configkit.LoadError", leftErr)
|
|
}
|
|
invalidRight, rightErr := configkit.Load[facadeDiffSecretConfig](configkit.Bytes("invalid-right.yml", []byte("unknown: true\n")))
|
|
if invalidRight != nil || rightErr == nil {
|
|
t.Fatalf("invalid right load = %#v, %v", invalidRight, rightErr)
|
|
}
|
|
if !errors.As(rightErr, &loadError) {
|
|
t.Fatalf("right load error = %T, want *configkit.LoadError", rightErr)
|
|
}
|
|
}
|