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) } }