Files
commandkit/integrations/ckconfig/diff_report_render_test.go

174 lines
8.1 KiB
Go

package ckconfig
import (
"bytes"
"reflect"
"strings"
"testing"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
"go.yaml.in/yaml/v4"
)
func renderAndDecode(t *testing.T, differences []configkit.Difference) (*yaml.Node, []byte) {
t.Helper()
report, err := renderDiffReport(differences)
if err != nil {
t.Fatal(err)
}
var document yaml.Node
if err := yaml.Unmarshal(report, &document); err != nil {
t.Fatalf("decode rendered report %q: %v", report, err)
}
return document.Content[0], report
}
func reportField(mapping *yaml.Node, key string) *yaml.Node {
for index := 0; index+1 < len(mapping.Content); index += 2 {
if mapping.Content[index].Value == key {
return mapping.Content[index+1]
}
}
return nil
}
func TestRenderDiffReportClassifiesAndOrdersNativeRecords(t *testing.T) {
differences := []configkit.Difference{
{Path: configkit.Keys("z-added"), Right: configkit.ValueSnapshot{Present: true, YAML: "false\n"}},
{Path: configkit.Keys("count"), Left: configkit.ValueSnapshot{Present: true, YAML: "1\n"}, Right: configkit.ValueSnapshot{Present: true, YAML: "9007199254740993\n"}},
{Path: configkit.Keys("removed"), Left: configkit.ValueSnapshot{Present: true, YAML: "unused"}},
{Path: configkit.Keys("a-added"), Right: configkit.ValueSnapshot{Present: true, YAML: "'123'\n"}},
}
root, _ := renderAndDecode(t, differences)
if root.Kind != yaml.MappingNode || len(root.Content) != 6 {
t.Fatalf("report root = %#v", root)
}
if got := []string{root.Content[0].Value, root.Content[2].Value, root.Content[4].Value}; !reflect.DeepEqual(got, []string{"additions", "changes", "deletions"}) {
t.Fatalf("report category order = %#v", got)
}
additions := reportField(root, "additions")
if additions.Kind != yaml.MappingNode || additions.Content[0].Value != "z-added" || additions.Content[2].Value != "a-added" {
t.Fatalf("addition order/value = %#v", additions)
}
if additions.Content[1].Kind != yaml.ScalarNode || additions.Content[1].Tag != "!!bool" || additions.Content[1].Value != "false" {
t.Fatalf("boolean addition lost its type: %#v", additions.Content[1])
}
if additions.Content[3].Tag != "!!str" || additions.Content[3].Value != "123" {
t.Fatalf("numeric-looking string lost its type: %#v", additions.Content[3])
}
changes := reportField(root, "changes")
if changes.Kind != yaml.MappingNode || changes.Content[1].Tag != "!!int" || changes.Content[1].Value != "9007199254740993" {
t.Fatalf("change value/type = %#v", changes)
}
deletions := reportField(root, "deletions")
if deletions.Kind != yaml.SequenceNode || len(deletions.Content) != 1 || deletions.Content[0].Tag != "!!str" || deletions.Content[0].Value != "/removed" {
t.Fatalf("deletion records = %#v", deletions)
}
}
func TestRenderDiffReportEmitsEmptyCategoriesAndRootValue(t *testing.T) {
root, _ := renderAndDecode(t, nil)
if got := []string{root.Content[0].Value, root.Content[2].Value, root.Content[4].Value}; !reflect.DeepEqual(got, []string{"additions", "changes", "deletions"}) {
t.Fatalf("empty report keys = %#v", got)
}
if reportField(root, "additions").Kind != yaml.MappingNode || len(reportField(root, "additions").Content) != 0 || reportField(root, "changes").Kind != yaml.MappingNode || len(reportField(root, "changes").Content) != 0 || reportField(root, "deletions").Kind != yaml.SequenceNode || len(reportField(root, "deletions").Content) != 0 {
t.Fatalf("empty report categories = %#v", root)
}
root, _ = renderAndDecode(t, []configkit.Difference{{Right: configkit.ValueSnapshot{Present: true, YAML: "null\n"}}})
additions := reportField(root, "additions")
if additions.Tag != "!!null" || additions.Value != "null" {
t.Fatalf("whole-root addition was wrapped: %#v", additions)
}
}
func TestRenderDiffReportDistinguishesDeletionFromNullAndEmptyMappings(t *testing.T) {
differences := []configkit.Difference{
{Path: configkit.Keys("nullable"), Right: configkit.ValueSnapshot{Present: true, YAML: "null\n"}},
{Path: configkit.Keys("settings", "first"), Left: configkit.ValueSnapshot{Present: true, YAML: "intentionally not parsed"}},
{Path: configkit.Keys("settings", "second"), Left: configkit.ValueSnapshot{Present: true, YAML: "also not parsed"}},
}
root, _ := renderAndDecode(t, differences)
if got := reportField(root, "additions").Content; len(got) != 2 || got[0].Value != "nullable" || got[1].Tag != "!!null" {
t.Fatalf("explicit null addition = %#v", got)
}
deletions := reportField(root, "deletions")
if len(deletions.Content) != 2 || deletions.Content[0].Value != "/settings/first" || deletions.Content[1].Value != "/settings/second" {
t.Fatalf("mapping-to-empty deletion records = %#v", deletions)
}
}
func TestRenderDiffReportKeepsAtomicSequencesAndStructuralValues(t *testing.T) {
differences := []configkit.Difference{
{Path: configkit.Keys("items"), Left: configkit.ValueSnapshot{Present: true}, Right: configkit.ValueSnapshot{Present: true, YAML: "- one\n- two\n"}},
{Path: configkit.Keys("settings"), Left: configkit.ValueSnapshot{Present: true}, Right: configkit.ValueSnapshot{Present: true, YAML: "nested:\n enabled: true\n"}},
}
root, _ := renderAndDecode(t, differences)
changes := reportField(root, "changes")
if changes.Content[1].Kind != yaml.SequenceNode || len(changes.Content[1].Content) != 2 {
t.Fatalf("sequence change was expanded or altered: %#v", changes.Content[1])
}
settings := reportField(changes, "settings")
if settings.Kind != yaml.MappingNode || reportField(settings, "nested").Kind != yaml.MappingNode || reportField(reportField(settings, "nested"), "enabled").Tag != "!!bool" {
t.Fatalf("structural replacement was altered: %#v", settings)
}
}
func TestRenderDiffReportPreservesNativeMasksAndMarkerLookingStrings(t *testing.T) {
type settings struct {
Public string `yaml:"public"`
Secret string `yaml:"secret"`
}
definition := configkit.Definition[settings]{Secret: func(path configkit.Path) bool { return path.String() == "/secret" }}
load := func(public, secret string) *configkit.Result[settings] {
result, err := configkit.Load(configkit.Mapping("configuration", map[string]any{"public": public, "secret": secret}), configkit.Options[settings]{Definition: definition})
if err != nil {
t.Fatal(err)
}
return result
}
left := load("visible", "old-secret")
right := load("[REDACTED]", "new-secret")
differences, err := configkit.Diff(left, right)
if err != nil {
t.Fatal(err)
}
if len(differences) != 2 || differences[0].Path.String() != "/public" || differences[1].Path.String() != "/secret" || !differences[1].HiddenChanged || !differences[1].Right.Redacted {
t.Fatalf("native changes/mask metadata = %#v", differences)
}
root, report := renderAndDecode(t, differences)
changes := reportField(root, "changes")
for _, key := range []string{"public", "secret"} {
value := reportField(changes, key)
if value == nil || value.Tag != "!!str" || value.Value != "[REDACTED]" {
t.Fatalf("safe value for %q = %#v", key, value)
}
}
if bytes.Contains(report, []byte("HiddenChanged")) || bytes.Contains(report, []byte("Redacted")) || strings.Contains(string(report), "old-secret") || strings.Contains(string(report), "new-secret") {
t.Fatalf("report exposed native metadata or secret contents: %q", report)
}
concealedOnly := load("[REDACTED]", "later-secret")
concealedDifferences, err := configkit.Diff(right, concealedOnly)
if err != nil {
t.Fatal(err)
}
if len(concealedDifferences) != 1 || concealedDifferences[0].Path.String() != "/secret" || !concealedDifferences[0].HiddenChanged {
t.Fatalf("concealed-only native records = %#v", concealedDifferences)
}
concealedRoot, _ := renderAndDecode(t, concealedDifferences)
if value := reportField(reportField(concealedRoot, "changes"), "secret"); value == nil || value.Tag != "!!str" || value.Value != "[REDACTED]" {
t.Fatalf("concealed-only safe value = %#v", value)
}
}
func TestRenderDiffReportReportsMalformedPresentSnapshot(t *testing.T) {
report, err := renderDiffReport([]configkit.Difference{{
Path: configkit.Keys("setting"),
Right: configkit.ValueSnapshot{Present: true, YAML: "setting: [unterminated\n"},
}})
if err == nil || report != nil || !strings.Contains(err.Error(), "/setting") {
t.Fatalf("malformed snapshot report/error = %q / %v", report, err)
}
}