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