From 710f0fb3527ea9f0f2a2e02994b45e1c52e46e80 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Fri, 9 Oct 2026 10:48:17 +0200 Subject: [PATCH] fix(config): map invalid ladder amendments to ErrLadderAmendInvalid - AmendLadderTranche returned the plain Validate() error, so callers other than the HTTP handler could not tell a bad amendment from a server fault. The error now matches ErrLadderAmendInvalid via errors.Is while keeping the specific validation message (the sentinel text is about the run total and cap, which would mislead). - Unit test covers the mapping without a database and fails without it. Refs #668 --- internal/config/ladder_amend.go | 8 ++++++++ .../config/store_postgres_ladder_amend.go | 2 +- .../store_postgres_ladder_amend_test.go | 20 +++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 internal/config/store_postgres_ladder_amend_test.go diff --git a/internal/config/ladder_amend.go b/internal/config/ladder_amend.go index 35e085882..f80a0614f 100644 --- a/internal/config/ladder_amend.go +++ b/internal/config/ladder_amend.go @@ -15,6 +15,14 @@ var ( ladderAmountPattern = regexp.MustCompile(`^(0|[1-9]\d{0,13})(\.\d{1,6})?$`) ) +// ladderAmendValidationError keeps the Validate message for the caller while +// matching ErrLadderAmendInvalid, so store callers can map every invalid +// amendment to a client error with errors.Is. +type ladderAmendValidationError struct{ err error } + +func (e ladderAmendValidationError) Error() string { return e.err.Error() } +func (e ladderAmendValidationError) Unwrap() []error { return []error{ErrLadderAmendInvalid, e.err} } + type LadderAmendment struct { ExpectedRevision *int64 `json:"expected_revision"` ScheduledDate time.Time `json:"scheduled_date"` diff --git a/internal/config/store_postgres_ladder_amend.go b/internal/config/store_postgres_ladder_amend.go index 0c71a534b..1995a4f43 100644 --- a/internal/config/store_postgres_ladder_amend.go +++ b/internal/config/store_postgres_ladder_amend.go @@ -24,7 +24,7 @@ func (s *PostgresStore) LadderTrancheScope(ctx context.Context, id string) (acco func (s *PostgresStore) AmendLadderTranche(ctx context.Context, id, accountID, provider, actor string, amendment LadderAmendment) (*LadderAmendmentResult, error) { if err := amendment.Validate(); err != nil { - return nil, err + return nil, ladderAmendValidationError{err} } if actor == "" { return nil, errors.New("ladder amendment actor is required") diff --git a/internal/config/store_postgres_ladder_amend_test.go b/internal/config/store_postgres_ladder_amend_test.go new file mode 100644 index 000000000..d6226b921 --- /dev/null +++ b/internal/config/store_postgres_ladder_amend_test.go @@ -0,0 +1,20 @@ +package config + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAmendLadderTranche_InvalidAmendmentMatchesErrLadderAmendInvalid(t *testing.T) { + zero := int64(0) + store := &PostgresStore{} // validation fails before the database is used + _, err := store.AmendLadderTranche(context.Background(), "id", "account", "aws", "actor", + LadderAmendment{ExpectedRevision: &zero, ScheduledDate: time.Now(), AmountUSDHr: "0"}) + require.ErrorIs(t, err, ErrLadderAmendInvalid) + assert.Contains(t, err.Error(), "amount_usd_hr must be a positive decimal") + assert.NotContains(t, err.Error(), "per-run cap") +}