package ckconfig import ( "bytes" "context" "errors" "fmt" "os" "path/filepath" "strings" "testing" commandkit "gitea.maximumdirect.net/eric/CommandKit" configkit "gitea.maximumdirect.net/eric/ConfigKit" ) type adapterSettings struct { Greeting string `yaml:"greeting"` Secret string `yaml:"secret"` } func adapterDefinition() configkit.Definition[adapterSettings] { return configkit.Definition[adapterSettings]{ Defaults: func() adapterSettings { return adapterSettings{Greeting: "Hello"} }, Validate: func(value *adapterSettings, validation *configkit.Validation) { if value.Greeting == "Hello" { validation.Warning(configkit.Keys("greeting"), "using the default greeting") } }, Secret: func(path configkit.Path) bool { return path.String() == "/secret" }, } } func adapterSettingsLoad(source configkit.Source) (*configkit.Result[adapterSettings], error) { return configkit.Load(source, configkit.Options[adapterSettings]{Definition: adapterDefinition()}) } func comparisonSide(selection Selection, greeting string) ComparisonSide[struct{ Label string }] { return ComparisonSide[struct{ Label string }]{ Selection: selection, Input: struct{ Label string }{Label: greeting}, } } func TestFromConfigKitCompareReturnsNativeChangesAndLoadsEqualSelections(t *testing.T) { selection := Selection{Config: commandkit.Supplied[string]{Value: "same.yml", Set: true}} var calls []struct { selection Selection input struct{ Label string } state *int } scope := FromConfigKit[struct{ Label string }, *int, adapterSettings]("app", "Application configuration", func(_ context.Context, selected Selection, input struct{ Label string }, state *int) (*configkit.Result[adapterSettings], error) { calls = append(calls, struct { selection Selection input struct{ Label string } state *int }{selected, input, state}) *state++ return adapterSettingsLoad(configkit.Mapping(input.Label, map[string]any{"greeting": input.Label})) }) state := new(int) acquired, err := scope.Compare(context.Background(), ComparisonRequest[struct{ Label string }]{ Left: comparisonSide(selection, "before"), Right: comparisonSide(selection, "after"), }, state) if err != nil { t.Fatal(err) } if len(calls) != 2 || calls[0].selection != selection || calls[1].selection != selection { t.Fatalf("loader calls = %#v", calls) } if calls[0].input.Label != "before" || calls[1].input.Label != "after" || calls[0].state != state || calls[1].state != state || *state != 2 { t.Fatalf("loader inputs/state = %#v, %#v, state=%d", calls[0], calls[1], *state) } if len(acquired.Value) != 1 || acquired.Value[0].Path.String() != "/greeting" || acquired.Value[0].Left.YAML != "before\n" || acquired.Value[0].Right.YAML != "after\n" { t.Fatalf("native differences = %#v", acquired.Value) } equal, err := scope.Compare(context.Background(), ComparisonRequest[struct{ Label string }]{ Left: comparisonSide(selection, "same"), Right: comparisonSide(selection, "same"), }, state) if err != nil { t.Fatal(err) } if len(equal.Value) != 0 || len(calls) != 4 { t.Fatalf("equal comparison records/calls = %#v / %d", equal.Value, len(calls)) } } func TestFromConfigKitCompareIgnoresDefaultAndResolutionHistoryForEqualValues(t *testing.T) { type resolvedSettings struct { Greeting string `yaml:"greeting"` } definition := configkit.Definition[resolvedSettings]{ Defaults: func() resolvedSettings { return resolvedSettings{Greeting: "resolved"} }, Resolve: func(value *resolvedSettings, resolution *configkit.Resolution) error { if value.Greeting != "resolved" { value.Greeting = "resolved" resolution.Record(configkit.Keys("greeting"), configkit.ChangeNormalization, "canonical greeting") } return nil }, } left, err := configkit.Load(configkit.NoSource(), configkit.Options[resolvedSettings]{Definition: definition}) if err != nil { t.Fatal(err) } right, err := configkit.Load(configkit.Mapping("right-source", map[string]any{"greeting": "source value"}), configkit.Options[resolvedSettings]{Definition: definition}) if err != nil { t.Fatal(err) } scope := FromConfigKit[struct{}, struct{}, resolvedSettings]("app", "Application configuration", func(_ context.Context, selected Selection, _ struct{}, _ struct{}) (*configkit.Result[resolvedSettings], error) { if selected.Config.Value == "left" { return left, nil } return right, nil }) acquired, err := scope.Compare(context.Background(), ComparisonRequest[struct{}]{ Left: ComparisonSide[struct{}]{Selection: Selection{Config: commandkit.Supplied[string]{Value: "left", Set: true}}}, Right: ComparisonSide[struct{}]{Selection: Selection{Config: commandkit.Supplied[string]{Value: "right", Set: true}}}, }, struct{}{}) if err != nil { t.Fatal(err) } if left.Config != right.Config || left.Config.Greeting != "resolved" || len(acquired.Value) != 0 { t.Fatalf("equal resolved values yielded differences: left=%#v right=%#v records=%#v", left.Config, right.Config, acquired.Value) } } func TestFromConfigKitCompareUsesBaseAndOverlaySelections(t *testing.T) { dir := t.TempDir() basePath := filepath.Join(dir, "base.yml") overlayPath := filepath.Join(dir, "overlays", "testing.yml") if err := os.WriteFile(basePath, []byte("greeting: Hello\n"), 0o600); err != nil { t.Fatal(err) } if err := os.Mkdir(filepath.Dir(overlayPath), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(overlayPath, []byte("greeting: Goodbye\n"), 0o600); err != nil { t.Fatal(err) } loader := func(_ context.Context, selection Selection, _ struct{}, _ struct{}) (*configkit.Result[adapterSettings], error) { options := configkit.LayeredOptions[adapterSettings]{Definition: adapterDefinition()} if selection.Overlay.Set { options.Overlay = selection.Overlay.Value options.OverlayFilesystem = &configkit.FilesystemContext{Root: dir, BaseDir: dir} } return configkit.LoadLayered(configkit.Input{Source: configkit.File(selection.Config.Value)}, options) } scope := FromConfigKit[struct{}, struct{}, adapterSettings]("app", "Application configuration", loader) acquired, err := scope.Compare(context.Background(), ComparisonRequest[struct{}]{ Left: ComparisonSide[struct{}]{Selection: Selection{Config: commandkit.Supplied[string]{Value: basePath, Set: true}}}, Right: ComparisonSide[struct{}]{Selection: Selection{ Config: commandkit.Supplied[string]{Value: basePath, Set: true}, Overlay: commandkit.Supplied[string]{Value: "testing", Set: true}, }}, }, struct{}{}) if err != nil { t.Fatal(err) } if len(acquired.Value) != 1 || acquired.Value[0].Path.String() != "/greeting" || acquired.Value[0].Left.YAML != "Hello\n" || acquired.Value[0].Right.YAML != "Goodbye\n" { t.Fatalf("base/overlay differences = %#v", acquired.Value) } } func TestFromConfigKitCompareRetainsSideDiagnosticsAndLoadError(t *testing.T) { left, err := adapterSettingsLoad(configkit.Mapping("left-source", map[string]any{"greeting": "Hello"})) if err != nil { t.Fatal(err) } _, loadErr := configkit.Load(configkit.Mapping("right-source", map[string]any{"unknown": true}), configkit.Options[adapterSettings]{Definition: adapterDefinition()}) if loadErr == nil { t.Fatal("invalid right source unexpectedly loaded") } wrapper := fmt.Errorf("application loader context: %w", loadErr) scope := FromConfigKit[struct{}, struct{}, adapterSettings]("app", "Application configuration", func(_ context.Context, selected Selection, _ struct{}, _ struct{}) (*configkit.Result[adapterSettings], error) { if selected.Config.Value == "left" { return left, nil } return nil, wrapper }) acquired, gotErr := scope.Compare(context.Background(), ComparisonRequest[struct{}]{ Left: ComparisonSide[struct{}]{Selection: Selection{Config: commandkit.Supplied[string]{Value: "left", Set: true}}}, Right: ComparisonSide[struct{}]{Selection: Selection{Config: commandkit.Supplied[string]{Value: "right", Set: true}}}, }, struct{}{}) if !errors.Is(gotErr, loadErr) { t.Fatalf("load error identity not preserved: %v", gotErr) } var typed *configkit.LoadError if !errors.As(gotErr, &typed) || typed != loadErr { t.Fatalf("load error type not preserved: %#v", typed) } if len(acquired.Diagnostics) != 2 || acquired.Diagnostics[0].Severity != commandkit.SeverityWarning || acquired.Diagnostics[1].Severity != commandkit.SeverityError { t.Fatalf("retained diagnostics = %#v", acquired.Diagnostics) } var leftText, rightText bytes.Buffer if err := acquired.Diagnostics[0].Write(&leftText); err != nil { t.Fatal(err) } if err := acquired.Diagnostics[1].Write(&rightText); err != nil { t.Fatal(err) } if !strings.Contains(leftText.String(), `Configuration scope "app" left:`) || !strings.Contains(leftText.String(), `"left-source"`) { t.Fatalf("left diagnostic lost side/source identity: %q", leftText.String()) } if !strings.Contains(rightText.String(), `Configuration scope "app" right:`) || !strings.Contains(rightText.String(), `"right-source"`) { t.Fatalf("right diagnostic lost side/source identity: %q", rightText.String()) } } func TestFromConfigKitCompareStopsAfterLeftLoadFailure(t *testing.T) { _, loadErr := configkit.Load(configkit.Mapping("left-source", map[string]any{"unknown": true}), configkit.Options[adapterSettings]{Definition: adapterDefinition()}) if loadErr == nil { t.Fatal("invalid left source unexpectedly loaded") } rightCalled := false scope := FromConfigKit[struct{}, struct{}, adapterSettings]("app", "Application configuration", func(_ context.Context, selected Selection, _ struct{}, _ struct{}) (*configkit.Result[adapterSettings], error) { if selected.Config.Value == "left" { return nil, loadErr } rightCalled = true return adapterSettingsLoad(configkit.Mapping("right-source", map[string]any{"greeting": "Goodbye"})) }) acquired, err := scope.Compare(context.Background(), ComparisonRequest[struct{}]{ Left: ComparisonSide[struct{}]{Selection: Selection{Config: commandkit.Supplied[string]{Value: "left", Set: true}}}, Right: ComparisonSide[struct{}]{Selection: Selection{Config: commandkit.Supplied[string]{Value: "right", Set: true}}}, }, struct{}{}) if !errors.Is(err, loadErr) || rightCalled { t.Fatalf("left failure/right load = %v / %v", err, rightCalled) } var typed *configkit.LoadError if !errors.As(err, &typed) || typed != loadErr || len(acquired.Diagnostics) != 1 || acquired.Diagnostics[0].Severity != commandkit.SeverityError { t.Fatalf("left failure identity/diagnostics = %v / %#v", err, acquired.Diagnostics) } } func TestFromConfigKitCompareCancellationStopsBeforeRightLoad(t *testing.T) { left, err := adapterSettingsLoad(configkit.Mapping("left-source", map[string]any{"greeting": "Hello"})) if err != nil { t.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() rightCalled := false scope := FromConfigKit[struct{}, struct{}, adapterSettings]("app", "Application configuration", func(_ context.Context, selection Selection, _ struct{}, _ struct{}) (*configkit.Result[adapterSettings], error) { if selection.Config.Value == "left" { cancel() return left, nil } rightCalled = true return adapterSettingsLoad(configkit.Mapping("right-source", map[string]any{"greeting": "Goodbye"})) }) acquired, err := scope.Compare(ctx, ComparisonRequest[struct{}]{ Left: ComparisonSide[struct{}]{Selection: Selection{Config: commandkit.Supplied[string]{Value: "left", Set: true}}}, Right: ComparisonSide[struct{}]{Selection: Selection{Config: commandkit.Supplied[string]{Value: "right", Set: true}}}, }, struct{}{}) if !errors.Is(err, context.Canceled) || rightCalled { t.Fatalf("cancellation/right load = %v / %v", err, rightCalled) } if len(acquired.Diagnostics) != 1 || acquired.Diagnostics[0].Severity != commandkit.SeverityWarning { t.Fatalf("left diagnostics lost on cancellation: %#v", acquired.Diagnostics) } } func TestFromConfigKitCompareRejectsNilResultAndRetainsNativeDiffFailureDiagnostics(t *testing.T) { t.Run("nil result", func(t *testing.T) { scope := FromConfigKit[struct{}, struct{}, adapterSettings]("app", "Application configuration", func(context.Context, Selection, struct{}, struct{}) (*configkit.Result[adapterSettings], error) { return nil, nil }) _, err := scope.Compare(context.Background(), ComparisonRequest[struct{}]{}, struct{}{}) if err == nil || !strings.Contains(err.Error(), `"app" left`) || !strings.Contains(err.Error(), "nil result") { t.Fatalf("nil left result error = %v", err) } }) t.Run("native diff error", func(t *testing.T) { left, err := adapterSettingsLoad(configkit.Mapping("left-source", map[string]any{"greeting": "Hello"})) if err != nil { t.Fatal(err) } scope := FromConfigKit[struct{}, struct{}, adapterSettings]("app", "Application configuration", func(_ context.Context, selection Selection, _ struct{}, _ struct{}) (*configkit.Result[adapterSettings], error) { if selection.Config.Value == "left" { return left, nil } return &configkit.Result[adapterSettings]{}, nil }) acquired, err := scope.Compare(context.Background(), ComparisonRequest[struct{}]{ Left: ComparisonSide[struct{}]{Selection: Selection{Config: commandkit.Supplied[string]{Value: "left", Set: true}}}, Right: ComparisonSide[struct{}]{Selection: Selection{Config: commandkit.Supplied[string]{Value: "right", Set: true}}}, }, struct{}{}) if err == nil || !strings.Contains(err.Error(), "native diff") { t.Fatalf("native diff error = %v", err) } if len(acquired.Diagnostics) != 1 || acquired.Diagnostics[0].Severity != commandkit.SeverityWarning { t.Fatalf("native diff failure lost diagnostics: %#v", acquired.Diagnostics) } }) } func TestFromConfigKitComparePreservesConcealedNativeDifference(t *testing.T) { left, err := adapterSettingsLoad(configkit.Mapping("left-source", map[string]any{"secret": "old"})) if err != nil { t.Fatal(err) } right, err := adapterSettingsLoad(configkit.Mapping("right-source", map[string]any{"secret": "new"})) if err != nil { t.Fatal(err) } scope := FromConfigKit[struct{}, struct{}, adapterSettings]("app", "Application configuration", func(_ context.Context, selected Selection, _ struct{}, _ struct{}) (*configkit.Result[adapterSettings], error) { if selected.Config.Value == "left" { return left, nil } return right, nil }) acquired, err := scope.Compare(context.Background(), ComparisonRequest[struct{}]{ Left: ComparisonSide[struct{}]{Selection: Selection{Config: commandkit.Supplied[string]{Value: "left", Set: true}}}, Right: ComparisonSide[struct{}]{Selection: Selection{Config: commandkit.Supplied[string]{Value: "right", Set: true}}}, }, struct{}{}) if err != nil { t.Fatal(err) } if len(acquired.Value) != 1 || !acquired.Value[0].HiddenChanged || !acquired.Value[0].Left.Redacted || !acquired.Value[0].Right.Redacted || acquired.Value[0].Left.YAML != acquired.Value[0].Right.YAML { t.Fatalf("concealed native difference was altered: %#v", acquired.Value) } }