This commit is contained in:
Stephan D
2026-03-10 12:31:09 +01:00
parent d87e709f43
commit e77d1ab793
287 changed files with 2089 additions and 1550 deletions

View File

@@ -0,0 +1,47 @@
version: "2"
linters:
default: none
enable:
- bodyclose
- canonicalheader
- copyloopvar
- durationcheck
- errcheck
- errchkjson
- errname
- errorlint
- gosec
- govet
- ineffassign
- nilerr
- nilnesserr
- nilnil
- noctx
- rowserrcheck
- sqlclosecheck
- staticcheck
- unconvert
- wastedassign
disable:
- depguard
- exhaustruct
- gochecknoglobals
- gochecknoinits
- gomoddirectives
- wrapcheck
- cyclop
- dupl
- funlen
- gocognit
- gocyclo
- ireturn
- lll
- mnd
- nestif
- nlreturn
- noinlineerr
- paralleltest
- tagliatelle
- testpackage
- varnamelen
- wsl_v5

View File

@@ -87,7 +87,7 @@ func (g *gatewayClient) callContext(ctx context.Context, method string) (context
}
g.logger.Info("Aurora gateway client call timeout applied", fields...)
}
return context.WithTimeout(ctx, timeout)
return context.WithTimeout(ctx, timeout) //nolint:gosec // cancel func is always invoked by call sites
}
func (g *gatewayClient) CreateCardPayout(ctx context.Context, req *mntxv1.CardPayoutRequest) (*mntxv1.CardPayoutResponse, error) {

View File

@@ -434,7 +434,7 @@ func buildGatewayLimits(cfg limitsConfig) *gatewayv1.Limits {
if bucket == "" {
continue
}
limits.VelocityLimit[bucket] = int32(value)
limits.VelocityLimit[bucket] = int32(value) //nolint:gosec // velocity limits are validated config values
}
}
@@ -450,7 +450,7 @@ func buildGatewayLimits(cfg limitsConfig) *gatewayv1.Limits {
MinAmount: strings.TrimSpace(override.MinAmount),
MaxAmount: strings.TrimSpace(override.MaxAmount),
MaxFee: strings.TrimSpace(override.MaxFee),
MaxOps: int32(override.MaxOps),
MaxOps: int32(override.MaxOps), //nolint:gosec // max ops is a validated config value
}
}
}
@@ -546,11 +546,12 @@ func (i *Imp) startHTTPCallbackServer(svc *auroraservice.Service, cfg callbackRu
})
server := &http.Server{
Addr: cfg.Address,
Handler: router,
Addr: cfg.Address,
Handler: router,
ReadHeaderTimeout: 5 * time.Second,
}
ln, err := net.Listen("tcp", cfg.Address)
ln, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp", cfg.Address)
if err != nil {
return err
}

View File

@@ -53,7 +53,7 @@ func (s *cardPayoutStore) FindByIdempotencyKey(_ context.Context, key string) (*
return v, nil
}
}
return nil, nil
return nil, nil //nolint:nilnil // test store: payout not found by idempotency key
}
func (s *cardPayoutStore) FindByOperationRef(_ context.Context, ref string) (*model.CardPayout, error) {
@@ -64,7 +64,7 @@ func (s *cardPayoutStore) FindByOperationRef(_ context.Context, ref string) (*mo
return v, nil
}
}
return nil, nil
return nil, nil //nolint:nilnil // test store: payout not found by operation ref
}
func (s *cardPayoutStore) FindByPaymentID(_ context.Context, id string) (*model.CardPayout, error) {
@@ -75,7 +75,7 @@ func (s *cardPayoutStore) FindByPaymentID(_ context.Context, id string) (*model.
return v, nil
}
}
return nil, nil
return nil, nil //nolint:nilnil // test store: payout not found by payment id
}
func (s *cardPayoutStore) Upsert(_ context.Context, record *model.CardPayout) error {

View File

@@ -107,7 +107,7 @@ func findOperationRef(operationRef, payoutID string) string {
func (p *cardPayoutProcessor) findExistingPayoutState(ctx context.Context, state *model.CardPayout) (*model.CardPayout, error) {
if p == nil || state == nil {
return nil, nil
return nil, nil //nolint:nilnil // nil processor/state means there is no existing payout state to load
}
if opRef := strings.TrimSpace(state.OperationRef); opRef != "" {
existing, err := p.store.Payouts().FindByOperationRef(ctx, opRef)
@@ -122,12 +122,12 @@ func (p *cardPayoutProcessor) findExistingPayoutState(ctx context.Context, state
}
}
}
return nil, nil
return nil, nil //nolint:nilnil // nil means no payout state exists for the operation reference
}
func (p *cardPayoutProcessor) findAndMergePayoutState(ctx context.Context, state *model.CardPayout) (*model.CardPayout, error) {
if p == nil || state == nil {
return nil, nil
return nil, nil //nolint:nilnil // nil processor/state means there is no existing payout state to merge
}
existing, err := p.findExistingPayoutState(ctx, state)
if err != nil {
@@ -862,7 +862,7 @@ func (p *cardPayoutProcessor) retryContext() (context.Context, context.CancelFun
if timeout <= 0 {
return ctx, func() {}
}
return context.WithTimeout(ctx, timeout)
return context.WithTimeout(ctx, timeout) //nolint:gosec // cancel func is always invoked by caller
}
func (p *cardPayoutProcessor) runCardPayoutRetry(req *mntxv1.CardPayoutRequest, attempt uint32, maxAttempts uint32) {
@@ -1369,8 +1369,7 @@ func (p *cardPayoutProcessor) Tokenize(ctx context.Context, req *mntxv1.CardToke
zap.String("customer_id", strings.TrimSpace(req.GetCustomerId())),
)
cardInput, err := validateCardTokenizeRequest(req, p.config)
if err != nil {
if _, err := validateCardTokenizeRequest(req, p.config); err != nil {
p.logger.Warn("Card tokenization validation failed",
zap.String("request_id", req.GetRequestId()),
zap.String("customer_id", req.GetCustomerId()),
@@ -1379,14 +1378,15 @@ func (p *cardPayoutProcessor) Tokenize(ctx context.Context, req *mntxv1.CardToke
return nil, err
}
var err error
req = sanitizeCardTokenizeRequest(req)
cardInput := extractTokenizeCard(req)
projectID, err := p.resolveProjectID(req.GetProjectId(), "request_id", req.GetRequestId())
if err != nil {
return nil, err
}
req = sanitizeCardTokenizeRequest(req)
cardInput = extractTokenizeCard(req)
token := buildSimulatedCardToken(req.GetRequestId(), cardInput.pan)
maskedPAN := provider.MaskPAN(cardInput.pan)
p.rememberTokenPAN(token, cardInput.pan)
@@ -1506,7 +1506,8 @@ func (p *cardPayoutProcessor) ProcessCallback(ctx context.Context, payload []byt
}
retryScheduled := false
if state.Status == model.PayoutStatusFailed || state.Status == model.PayoutStatusCancelled {
switch state.Status {
case model.PayoutStatusFailed, model.PayoutStatusCancelled:
decision := p.retryPolicy.decideProviderFailure(state.ProviderCode)
attemptsUsed := p.currentDispatchAttempt(operationRef)
maxAttempts := p.maxDispatchAttempts()
@@ -1553,7 +1554,7 @@ func (p *cardPayoutProcessor) ProcessCallback(ctx context.Context, payload []byt
if !retryScheduled && strings.TrimSpace(state.FailureReason) == "" {
state.FailureReason = payoutFailureReason(state.ProviderCode, state.ProviderMessage)
}
} else if state.Status == model.PayoutStatusSuccess {
case model.PayoutStatusSuccess:
state.FailureReason = ""
}

View File

@@ -36,6 +36,15 @@ func (s staticClock) Now() time.Time {
return s.now
}
func mustMarshalJSON(t *testing.T, value any) []byte {
t.Helper()
body, err := json.Marshal(value)
if err != nil {
t.Fatalf("json marshal failed: %v", err)
}
return body
}
type apiResponse struct {
RequestID string `json:"request_id"`
Status string `json:"status"`
@@ -70,7 +79,7 @@ func TestCardPayoutProcessor_Submit_Success(t *testing.T) {
Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
resp := apiResponse{}
resp.Operation.RequestID = "req-123"
body, _ := json.Marshal(resp)
body := mustMarshalJSON(t, resp)
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader(body)),
@@ -260,7 +269,7 @@ func TestCardPayoutProcessor_Submit_SameParentDifferentOperationsStoredSeparatel
callN++
resp := apiResponse{}
resp.Operation.RequestID = fmt.Sprintf("req-%d", callN)
body, _ := json.Marshal(resp)
body := mustMarshalJSON(t, resp)
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader(body)),
@@ -350,7 +359,7 @@ func TestCardPayoutProcessor_StrictMode_BlocksSecondOperationUntilFirstFinalCall
n := callN.Add(1)
resp := apiResponse{}
resp.Operation.RequestID = fmt.Sprintf("req-%d", n)
body, _ := json.Marshal(resp)
body := mustMarshalJSON(t, resp)
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader(body)),
@@ -544,7 +553,7 @@ func TestCardPayoutProcessor_Submit_RetriesProviderLimitDeclineUntilSuccess(t *t
if n == 1 {
resp.Code = providerCodeDeclineAmountOrFrequencyLimit
resp.Message = "Decline due to amount or frequency limit"
body, _ := json.Marshal(resp)
body := mustMarshalJSON(t, resp)
return &http.Response{
StatusCode: http.StatusTooManyRequests,
Body: io.NopCloser(bytes.NewReader(body)),
@@ -552,7 +561,7 @@ func TestCardPayoutProcessor_Submit_RetriesProviderLimitDeclineUntilSuccess(t *t
}, nil
}
resp.Operation.RequestID = "req-retry-success"
body, _ := json.Marshal(resp)
body := mustMarshalJSON(t, resp)
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader(body)),
@@ -617,7 +626,7 @@ func TestCardPayoutProcessor_Submit_RetriesProviderLimitDeclineThenFails(t *test
Code: providerCodeDeclineAmountOrFrequencyLimit,
Message: "Decline due to amount or frequency limit",
}
body, _ := json.Marshal(resp)
body := mustMarshalJSON(t, resp)
return &http.Response{
StatusCode: http.StatusTooManyRequests,
Body: io.NopCloser(bytes.NewReader(body)),
@@ -689,7 +698,7 @@ func TestCardPayoutProcessor_ProcessCallback_RetryableDeclineSchedulesRetry(t *t
} else {
resp.Operation.RequestID = "req-after-callback-retry"
}
body, _ := json.Marshal(resp)
body := mustMarshalJSON(t, resp)
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader(body)),

View File

@@ -40,8 +40,8 @@ func TestValidateCardTokenizeRequest_Expired(t *testing.T) {
cfg := testProviderConfig()
req := validCardTokenizeRequest()
now := time.Now().UTC()
req.CardExpMonth = uint32(now.Month())
req.CardExpYear = uint32(now.Year() - 1)
req.CardExpMonth = uint32(now.Month()) //nolint:gosec // month value is bounded by time.Time
req.CardExpYear = uint32(now.Year() - 1) //nolint:gosec // test value intentionally uses previous year
_, err := validateCardTokenizeRequest(req, cfg)
requireReason(t, err, "expired_card")

View File

@@ -251,8 +251,8 @@ func buildCardPayoutRequestFromParams(reader params.Reader,
AmountMinor: amountMinor,
Currency: currency,
CardPan: strings.TrimSpace(reader.String("card_pan")),
CardExpYear: uint32(readerInt64(reader, "card_exp_year")),
CardExpMonth: uint32(readerInt64(reader, "card_exp_month")),
CardExpYear: uint32(readerInt64(reader, "card_exp_year")), //nolint:gosec // values are validated by request validators
CardExpMonth: uint32(readerInt64(reader, "card_exp_month")), //nolint:gosec // values are validated by request validators
CardHolder: strings.TrimSpace(reader.String("card_holder")),
Metadata: metadataFromReader(reader),
OperationRef: operationRef,

View File

@@ -128,12 +128,13 @@ func (m *strictIsolatedPayoutExecutionMode) tryAcquire(operationRef string) (<-c
return nil, false, errPayoutExecutionModeStopped
}
switch owner := strings.TrimSpace(m.activeOperation); {
case owner == "":
owner := strings.TrimSpace(m.activeOperation)
switch owner {
case "":
m.activeOperation = operationRef
m.signalLocked()
return nil, true, nil
case owner == operationRef:
case operationRef:
return nil, true, nil
default:
return m.waitCh, false, nil

View File

@@ -28,7 +28,6 @@ func TestPayoutFailurePolicy_DecideProviderFailure(t *testing.T) {
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Helper()
got := policy.decideProviderFailure(tc.code)

View File

@@ -1,7 +1,7 @@
package gateway
import (
"crypto/sha1"
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
@@ -238,6 +238,6 @@ func normalizeExpiryYear(year uint32) string {
func buildSimulatedCardToken(requestID, pan string) string {
input := strings.TrimSpace(requestID) + "|" + normalizeCardNumber(pan)
sum := sha1.Sum([]byte(input))
sum := sha256.Sum256([]byte(input))
return "aur_tok_" + hex.EncodeToString(sum[:8])
}

View File

@@ -174,7 +174,7 @@ func (s *Service) startDiscoveryAnnouncer() {
if strings.TrimSpace(announce.ID) == "" {
announce.ID = discovery.StablePaymentGatewayID(discovery.RailCardPayout)
}
s.announcer = discovery.NewAnnouncer(s.logger, s.producer, string(mservice.MntxGateway), announce)
s.announcer = discovery.NewAnnouncer(s.logger, s.producer, mservice.MntxGateway, announce)
s.announcer.Start()
}

View File

@@ -18,8 +18,8 @@ func requireReason(t *testing.T, err error, reason string) {
if !errors.Is(err, merrors.ErrInvalidArg) {
t.Fatalf("expected invalid argument error, got %v", err)
}
reasoned, ok := err.(payoutFailure)
if !ok {
var reasoned payoutFailure
if !errors.As(err, &reasoned) {
t.Fatalf("expected payout failure reason, got %T", err)
}
if reasoned.Reason() != reason {
@@ -82,5 +82,5 @@ func validCardTokenizeRequest() *mntxv1.CardTokenizeRequest {
func futureExpiry() (uint32, uint32) {
now := time.Now().UTC()
return uint32(now.Month()), uint32(now.Year() + 1)
return uint32(now.Month()), uint32(now.Year() + 1) //nolint:gosec // month/year values are bounded by time.Time
}

View File

@@ -65,7 +65,7 @@ func (p *cardPayoutProcessor) updatePayoutStatus(ctx context.Context, state *mod
return nil, emitErr
}
}
return nil, nil
return struct{}{}, nil
})
if err != nil {
p.logger.Warn("Failed to update transfer status", zap.Error(err), mzap.ObjRef("payout_ref", state.ID),