233 lines
8.5 KiB
Go
233 lines
8.5 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 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)
|
|
}
|
|
}
|