From 4acf378f96adc451115a1aafca83713bd5776f18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Dzivjak?= Date: Fri, 27 Mar 2026 22:06:35 +0100 Subject: [PATCH] fix(commands): handle nil list responses safely Add a shared helper for pointer-backed slice responses and use it in the remaining list commands that still dereferenced API results directly. This keeps empty or partial SDK responses from turning into nil-pointer panics and aligns them with the already-defensive transactions command. --- internal/commands/checkouts/checkouts.go | 5 +++-- internal/commands/customers/customers.go | 5 +++-- internal/commands/payouts/payouts.go | 6 ++++-- internal/commands/util/util.go | 7 +++++++ internal/commands/util/util_test.go | 14 ++++++++++++++ 5 files changed, 31 insertions(+), 6 deletions(-) diff --git a/internal/commands/checkouts/checkouts.go b/internal/commands/checkouts/checkouts.go index 8163c5b..bd7a82a 100644 --- a/internal/commands/checkouts/checkouts.go +++ b/internal/commands/checkouts/checkouts.go @@ -162,8 +162,9 @@ func listCheckouts(ctx context.Context, cmd *cli.Command) error { return display.PrintJSON(appCtx.Output, checkoutList) } - rows := make([][]attribute.Value, 0, len(*checkoutList)) - for _, checkout := range *checkoutList { + checkouts := util.SliceOrEmpty(checkoutList) + rows := make([][]attribute.Value, 0, len(checkouts)) + for _, checkout := range checkouts { status := "-" if checkout.Status != nil { status = string(*checkout.Status) diff --git a/internal/commands/customers/customers.go b/internal/commands/customers/customers.go index f64d71d..57e9e92 100644 --- a/internal/commands/customers/customers.go +++ b/internal/commands/customers/customers.go @@ -222,8 +222,9 @@ func listPaymentInstruments(ctx context.Context, cmd *cli.Command) error { return display.PrintJSON(appCtx.Output, instruments) } - rows := make([][]attribute.Value, 0, len(*instruments)) - for _, instrument := range *instruments { + paymentInstruments := util.SliceOrEmpty(instruments) + rows := make([][]attribute.Value, 0, len(paymentInstruments)) + for _, instrument := range paymentInstruments { rows = append(rows, []attribute.Value{ attribute.OptionalStringValue(instrument.Token), attribute.OptionalValue(instrument.Type), diff --git a/internal/commands/payouts/payouts.go b/internal/commands/payouts/payouts.go index 3bc490b..a1be5b9 100644 --- a/internal/commands/payouts/payouts.go +++ b/internal/commands/payouts/payouts.go @@ -12,6 +12,7 @@ import ( "github.com/sumup/sumup-go/datetime" "github.com/sumup/sumup-cli/internal/app" + "github.com/sumup/sumup-cli/internal/commands/util" "github.com/sumup/sumup-cli/internal/display" "github.com/sumup/sumup-cli/internal/display/attribute" ) @@ -97,8 +98,9 @@ func listPayouts(ctx context.Context, cmd *cli.Command) error { return display.PrintJSON(appCtx.Output, payoutList) } - rows := make([][]attribute.Value, 0, len(*payoutList)) - for _, payout := range *payoutList { + payouts := util.SliceOrEmpty(payoutList) + rows := make([][]attribute.Value, 0, len(payouts)) + for _, payout := range payouts { fee := attribute.OptionalValue(payout.Fee) if payout.Fee != nil { fee = attribute.ValueOf(fmt.Sprintf("%.2f", *payout.Fee)) diff --git a/internal/commands/util/util.go b/internal/commands/util/util.go index 60f2cf6..1d58e9d 100644 --- a/internal/commands/util/util.go +++ b/internal/commands/util/util.go @@ -58,3 +58,10 @@ func BoolLabel(value *bool) string { } return "No" } + +func SliceOrEmpty[S ~[]E, E any](value *S) S { + if value == nil { + return S{} + } + return *value +} diff --git a/internal/commands/util/util_test.go b/internal/commands/util/util_test.go index 467106d..187cf4d 100644 --- a/internal/commands/util/util_test.go +++ b/internal/commands/util/util_test.go @@ -83,6 +83,20 @@ func TestBoolLabel(t *testing.T) { }) } +func TestSliceOrEmpty(t *testing.T) { + t.Parallel() + + t.Run("returns empty slice for nil pointer", func(t *testing.T) { + var values *[]string + assert.Empty(t, util.SliceOrEmpty(values)) + }) + + t.Run("returns dereferenced slice when present", func(t *testing.T) { + values := []string{"a", "b"} + assert.Equal(t, values, util.SliceOrEmpty(&values)) + }) +} + func TestTimeOrDash(t *testing.T) { t.Parallel()