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

@@ -159,7 +159,7 @@ func (s *service) VerifyAccount(
token, err := s.vdb.Create(
ctx,
verification.NewLinkRequest(*acct.GetID(), model.PurposeAccountActivation, "").
WithTTL(time.Duration(time.Hour*24)),
WithTTL(time.Hour * 24),
)
if err != nil {
s.logger.Warn("Failed to create verification token for new account", zap.Error(err), mzap.StorableRef(acct))
@@ -238,7 +238,7 @@ func (s *service) ResetPassword(
return s.vdb.Create(
ctx,
verification.NewOTPRequest(*acct.GetID(), model.PurposePasswordReset, "").
WithTTL(time.Duration(time.Hour*1)),
WithTTL(time.Hour),
)
}
@@ -250,7 +250,7 @@ func (s *service) UpdateLogin(
return s.vdb.Create(
ctx,
verification.NewOTPRequest(*acct.GetID(), model.PurposeEmailChange, newLogin).
WithTTL(time.Duration(time.Hour*1)),
WithTTL(time.Hour),
)
}

View File

@@ -164,6 +164,7 @@ func (e Endpoint) DecodeIBAN() (IBANEndpoint, error) {
func LegacyPaymentEndpointToEndpointDTO(old *LegacyPaymentEndpoint) (*Endpoint, error) {
if old == nil {
//nolint:nilnil // Nil legacy endpoint means no endpoint provided.
return nil, nil
}
@@ -202,6 +203,7 @@ func LegacyPaymentEndpointToEndpointDTO(old *LegacyPaymentEndpoint) (*Endpoint,
func EndpointDTOToLegacyPaymentEndpoint(new *Endpoint) (*LegacyPaymentEndpoint, error) {
if new == nil {
//nolint:nilnil // Nil endpoint DTO means no endpoint provided.
return nil, nil
}

View File

@@ -344,7 +344,7 @@ func toPaymentOperation(step *orchestrationv2.StepExecution, quote *quotationv2.
Amount: amount,
ConvertedAmount: convertedAmount,
OperationRef: operationRef,
Gateway: string(gateway),
Gateway: gateway,
StartedAt: timestampAsTime(step.GetStartedAt()),
CompletedAt: timestampAsTime(step.GetCompletedAt()),
}
@@ -456,9 +456,9 @@ func gatewayTypeFromInstanceID(raw string) mservice.Type {
return ""
}
switch mservice.Type(value) {
switch value {
case mservice.ChainGateway, mservice.TronGateway, mservice.MntxGateway, mservice.PaymentGateway, mservice.TgSettle, mservice.Ledger:
return mservice.Type(value)
return value
}
switch {

View File

@@ -110,7 +110,7 @@ func Account2ClaimsForClient(a *model.Account, expiration int, clientID string)
paramNameName: t.Name,
paramNameLocale: t.Locale,
paramNameClientID: t.ClientID,
paramNameExpiration: int64(t.Expiration.Unix()),
paramNameExpiration: t.Expiration.Unix(),
paramNamePending: t.Pending,
}
}

View File

@@ -26,11 +26,11 @@ const (
var (
ledgerDiscoveryServiceNames = []string{
"LEDGER",
string(mservice.Ledger),
mservice.Ledger,
}
paymentOrchestratorDiscoveryServiceNames = []string{
"PAYMENTS_ORCHESTRATOR",
string(mservice.PaymentOrchestrator),
mservice.PaymentOrchestrator,
}
paymentQuotationDiscoveryServiceNames = []string{
"PAYMENTS_QUOTATION",
@@ -41,7 +41,7 @@ var (
paymentMethodsDiscoveryServiceNames = []string{
"PAYMENTS_METHODS",
"PAYMENT_METHODS",
string(mservice.PaymentMethods),
mservice.PaymentMethods,
}
)
@@ -339,13 +339,13 @@ func selectGatewayEndpoint(gateways []discovery.GatewaySummary, preferredNetwork
func parseDiscoveryInvokeURI(raw string) (discoveryEndpoint, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return discoveryEndpoint{}, fmt.Errorf("Invoke uri is empty")
return discoveryEndpoint{}, fmt.Errorf("invoke uri is empty")
}
// Without a scheme we expect a plain host:port target.
if !strings.Contains(raw, "://") {
if _, _, err := net.SplitHostPort(raw); err != nil {
return discoveryEndpoint{}, fmt.Errorf("Invoke uri must include host:port: %w", err)
return discoveryEndpoint{}, fmt.Errorf("invoke uri must include host:port: %w", err)
}
return discoveryEndpoint{
address: raw,
@@ -363,7 +363,7 @@ func parseDiscoveryInvokeURI(raw string) (discoveryEndpoint, error) {
case "grpc":
address := strings.TrimSpace(parsed.Host)
if _, _, splitErr := net.SplitHostPort(address); splitErr != nil {
return discoveryEndpoint{}, fmt.Errorf("Grpc invoke uri must include host:port: %w", splitErr)
return discoveryEndpoint{}, fmt.Errorf("grpc invoke uri must include host:port: %w", splitErr)
}
return discoveryEndpoint{
address: address,
@@ -373,7 +373,7 @@ func parseDiscoveryInvokeURI(raw string) (discoveryEndpoint, error) {
case "grpcs":
address := strings.TrimSpace(parsed.Host)
if _, _, splitErr := net.SplitHostPort(address); splitErr != nil {
return discoveryEndpoint{}, fmt.Errorf("Grpcs invoke uri must include host:port: %w", splitErr)
return discoveryEndpoint{}, fmt.Errorf("grpcs invoke uri must include host:port: %w", splitErr)
}
return discoveryEndpoint{
address: address,
@@ -388,7 +388,7 @@ func parseDiscoveryInvokeURI(raw string) (discoveryEndpoint, error) {
raw: raw,
}, nil
default:
return discoveryEndpoint{}, fmt.Errorf("Unsupported invoke uri scheme: %s", parsed.Scheme)
return discoveryEndpoint{}, fmt.Errorf("unsupported invoke uri scheme: %s", parsed.Scheme)
}
}

View File

@@ -43,6 +43,7 @@ func (d *DispatcherImpl) dispatchMessage(ctx context.Context, conn *websocket.Co
}
func (d *DispatcherImpl) handle(w http.ResponseWriter, r *http.Request) {
//nolint:contextcheck // websocket.Handler callback signature does not carry request context.
websocket.Handler(func(conn *websocket.Conn) {
ctx, cancel := context.WithTimeout(r.Context(), time.Duration(d.timeout)*time.Second)
defer cancel()

View File

@@ -71,6 +71,7 @@ func GetOptionalParam[T any](logger mlogger.Logger, r *http.Request, key string,
vals := r.URL.Query()
s := vals.Get(key)
if s == "" {
//nolint:nilnil // Missing optional query parameter is represented as (nil, nil).
return nil, nil
}

View File

@@ -1,17 +1,17 @@
package mutil
import (
"context"
"net/http"
"testing"
"github.com/tech/sendico/pkg/mlogger"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
)
func TestGetOptionalBoolParam(t *testing.T) {
logger := mlogger.Logger(zap.NewNop())
logger := zap.NewNop()
tests := []struct {
name string
@@ -47,7 +47,7 @@ func TestGetOptionalBoolParam(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req, err := http.NewRequest("GET", "http://example.com"+tt.query, nil)
req, err := http.NewRequestWithContext(context.Background(), "GET", "http://example.com"+tt.query, nil)
require.NoError(t, err)
result, err := GetOptionalBoolParam(logger, req, "param")
@@ -69,7 +69,7 @@ func TestGetOptionalBoolParam(t *testing.T) {
}
func TestGetOptionalInt64Param(t *testing.T) {
logger := mlogger.Logger(zap.NewNop())
logger := zap.NewNop()
tests := []struct {
name string
@@ -111,7 +111,7 @@ func TestGetOptionalInt64Param(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req, err := http.NewRequest("GET", "http://example.com"+tt.query, nil)
req, err := http.NewRequestWithContext(context.Background(), "GET", "http://example.com"+tt.query, nil)
require.NoError(t, err)
result, err := GetOptionalInt64Param(logger, req, "param")

View File

@@ -114,7 +114,7 @@ func (a *AccountAPI) deleteAll(r *http.Request, account *model.Account, token *s
a.logger.Warn("Failed to delete all data", zap.Error(err), mzap.StorableRef(&org), mzap.StorableRef(account))
return nil, err
}
return nil, nil
return struct{}{}, nil
}); err != nil {
a.logger.Warn("Failed to execute delete transaction", zap.Error(err), mzap.StorableRef(&org), mzap.StorableRef(account))
return response.Auto(a.logger, a.Name(), err)

View File

@@ -192,5 +192,5 @@ func (a *AccountAPI) resetPasswordTransactionBody(ctx context.Context, user *mod
// Don't fail the transaction if token revocation fails, but log it
}
return nil, nil
return struct{}{}, nil
}

View File

@@ -133,12 +133,7 @@ func TestPasswordValidationLogic(t *testing.T) {
for _, password := range invalidPasswords {
t.Run(password, func(t *testing.T) {
// Test that invalid passwords fail at least one requirement
isValid := true
// Check length
if len(password) < 8 {
isValid = false
}
isValid := len(password) >= 8
// Check for digit
hasDigit := false

View File

@@ -276,7 +276,7 @@ func (a *AccountAPI) grantAllPermissions(ctx context.Context, organizationRef bs
for resource, granted := range required {
if !granted {
a.logger.Warn("Required policy description not found for signup permissions", zap.String("resource", string(resource)))
a.logger.Warn("Required policy description not found for signup permissions", zap.String("resource", resource))
}
}

View File

@@ -126,7 +126,7 @@ func TestSignupHTTPSerialization(t *testing.T) {
require.NoError(t, err)
// Create HTTP request
req := httptest.NewRequest(http.MethodPost, "/signup", bytes.NewBuffer(reqBody))
req := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/signup", bytes.NewBuffer(reqBody))
req.Header.Set("Content-Type", "application/json")
// Parse the request body
@@ -162,7 +162,7 @@ func TestSignupHTTPSerialization(t *testing.T) {
t.Run("InvalidJSONRequest", func(t *testing.T) {
invalidJSON := `{"account": {"login": "test@example.com", "password": "invalid json structure`
req := httptest.NewRequest(http.MethodPost, "/signup", bytes.NewBufferString(invalidJSON))
req := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/signup", bytes.NewBufferString(invalidJSON))
req.Header.Set("Content-Type", "application/json")
var parsedRequest srequest.Signup

View File

@@ -37,7 +37,7 @@ func (a *CallbacksAPI) create(r *http.Request, account *model.Account, accessTok
if err := a.applySigningSecretMutation(ctx, *account.GetID(), *callback.GetID(), mutation); err != nil {
return nil, err
}
return nil, nil
return struct{}{}, nil
}); err != nil {
a.Logger.Warn("Failed to create callback transaction", zap.Error(err))
return response.Auto(a.Logger, a.Name(), err)

View File

@@ -49,7 +49,7 @@ func (a *CallbacksAPI) update(r *http.Request, account *model.Account, accessTok
if err := a.applySigningSecretMutation(ctx, *account.GetID(), callbackRef, mutation); err != nil {
return nil, err
}
return nil, nil
return struct{}{}, nil
}); err != nil {
a.Logger.Warn("Failed to update callback transaction", zap.Error(err))
return response.Auto(a.Logger, a.Name(), err)

View File

@@ -6,6 +6,7 @@ import (
"net/http"
"os"
"path/filepath"
"strings"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/domainprovider"
@@ -34,7 +35,10 @@ func (storage *LocalStorage) Delete(ctx context.Context, objID string) error {
default:
}
filePath := filepath.Join(storage.storageDir, objID)
filePath, err := storage.resolvePath(objID)
if err != nil {
return err
}
if err := os.Remove(filePath); err != nil {
if os.IsNotExist(err) {
storage.logger.Debug("File not found", zap.String("obj_ref", objID))
@@ -54,7 +58,11 @@ func (storage *LocalStorage) Save(ctx context.Context, file io.Reader, objID str
default:
}
filePath := filepath.Join(storage.storageDir, objID)
filePath, err := storage.resolvePath(objID)
if err != nil {
return "", err
}
//nolint:gosec // File path is resolved and constrained to storage root.
dst, err := os.Create(filePath)
if err != nil {
storage.logger.Warn("Error occurred while creating file", zap.Error(err), zap.String("storage", storage.storageDir), zap.String("obj_ref", objID))
@@ -78,7 +86,9 @@ func (storage *LocalStorage) Save(ctx context.Context, file io.Reader, objID str
}
case <-ctx.Done():
// Context was cancelled, clean up the partial file
os.Remove(filePath)
if removeErr := os.Remove(filePath); removeErr != nil && !os.IsNotExist(removeErr) {
storage.logger.Warn("Failed to remove partially written file", zap.Error(removeErr), zap.String("obj_ref", objID))
}
return "", ctx.Err()
}
@@ -93,7 +103,10 @@ func (storage *LocalStorage) Get(ctx context.Context, objRef string) http.Handle
default:
}
filePath := filepath.Join(storage.storageDir, objRef)
filePath, err := storage.resolvePath(objRef)
if err != nil {
return response.Internal(storage.logger, storage.service, err)
}
if _, err := os.Stat(filePath); err != nil {
storage.logger.Warn("Failed to access file", zap.Error(err), zap.String("storage", storage.storageDir), zap.String("obj_ref", objRef))
return response.Internal(storage.logger, storage.service, err)
@@ -117,7 +130,7 @@ func (storage *LocalStorage) Get(ctx context.Context, objRef string) http.Handle
func ensureDir(dirName string) error {
info, err := os.Stat(dirName)
if os.IsNotExist(err) {
return os.MkdirAll(dirName, 0o755)
return os.MkdirAll(dirName, 0o750)
}
if err != nil {
return err
@@ -128,6 +141,24 @@ func ensureDir(dirName string) error {
return nil
}
func (storage *LocalStorage) resolvePath(objID string) (string, error) {
objID = strings.TrimSpace(objID)
if objID == "" {
return "", merrors.InvalidArgument("obj_ref is required", "obj_ref")
}
filePath := filepath.Join(storage.storageDir, objID)
relPath, err := filepath.Rel(storage.storageDir, filePath)
if err != nil {
return "", merrors.InternalWrap(err, "failed to resolve local file path")
}
if relPath == "." || strings.HasPrefix(relPath, "..") {
return "", merrors.InvalidArgument("obj_ref is invalid", "obj_ref")
}
return filePath, nil
}
func CreateLocalFileStorage(logger mlogger.Logger, service mservice.Type, directory, subDir string, dp domainprovider.DomainProvider, cfg config.LocalFSSConfig) (*LocalStorage, error) {
dir := filepath.Join(cfg.RootPath, directory)
if err := ensureDir(dir); err != nil {

View File

@@ -55,7 +55,7 @@ func setupTestStorage(t *testing.T) (*LocalStorage, string, func()) {
// Return cleanup function
cleanup := func() {
os.RemoveAll(tempDir)
require.NoError(t, os.RemoveAll(tempDir))
}
return storage, tempDir, cleanup
@@ -81,7 +81,7 @@ func setupBenchmarkStorage(b *testing.B) (*LocalStorage, string, func()) {
// Return cleanup function
cleanup := func() {
os.RemoveAll(tempDir)
require.NoError(b, os.RemoveAll(tempDir))
}
return storage, tempDir, cleanup
@@ -138,6 +138,7 @@ func TestLocalStorage_Save(t *testing.T) {
// Verify file was actually saved
filePath := filepath.Join(tempDir, tt.objID)
//nolint:gosec // Test-controlled path inside temporary directory.
content, err := os.ReadFile(filePath)
assert.NoError(t, err)
assert.Equal(t, tt.content, string(content))
@@ -186,7 +187,7 @@ func TestLocalStorage_Delete(t *testing.T) {
// Create a test file
testFile := filepath.Join(tempDir, "test.txt")
err := os.WriteFile(testFile, []byte("test content"), 0o644)
err := os.WriteFile(testFile, []byte("test content"), 0o600)
require.NoError(t, err)
tests := []struct {
@@ -232,7 +233,7 @@ func TestLocalStorage_Delete_ContextCancellation(t *testing.T) {
// Create a test file
testFile := filepath.Join(tempDir, "test.txt")
err := os.WriteFile(testFile, []byte("test content"), 0o644)
err := os.WriteFile(testFile, []byte("test content"), 0o600)
require.NoError(t, err)
// Create a context that's already cancelled
@@ -256,7 +257,7 @@ func TestLocalStorage_Get(t *testing.T) {
// Create a test file
testContent := "test file content"
testFile := filepath.Join(tempDir, "test.txt")
err := os.WriteFile(testFile, []byte(testContent), 0o644)
err := os.WriteFile(testFile, []byte(testContent), 0o600)
require.NoError(t, err)
tests := []struct {
@@ -285,7 +286,7 @@ func TestLocalStorage_Get(t *testing.T) {
handler := storage.Get(ctx, tt.objID)
// Create test request
req := httptest.NewRequest("GET", "/files/"+tt.objID, nil)
req := httptest.NewRequestWithContext(context.Background(), "GET", "/files/"+tt.objID, nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
@@ -304,7 +305,7 @@ func TestLocalStorage_Get_ContextCancellation(t *testing.T) {
// Create a test file
testFile := filepath.Join(tempDir, "test.txt")
err := os.WriteFile(testFile, []byte("test content"), 0o644)
err := os.WriteFile(testFile, []byte("test content"), 0o600)
require.NoError(t, err)
// Create a context that's already cancelled
@@ -314,7 +315,7 @@ func TestLocalStorage_Get_ContextCancellation(t *testing.T) {
handler := storage.Get(ctx, "test.txt")
// Create test request
req := httptest.NewRequest("GET", "/files/test.txt", nil)
req := httptest.NewRequestWithContext(context.Background(), "GET", "/files/test.txt", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
@@ -328,14 +329,14 @@ func TestLocalStorage_Get_RequestContextCancellation(t *testing.T) {
// Create a test file
testFile := filepath.Join(tempDir, "test.txt")
err := os.WriteFile(testFile, []byte("test content"), 0o644)
err := os.WriteFile(testFile, []byte("test content"), 0o600)
require.NoError(t, err)
ctx := context.Background()
handler := storage.Get(ctx, "test.txt")
// Create test request with cancelled context
req := httptest.NewRequest("GET", "/files/test.txt", nil)
req := httptest.NewRequestWithContext(context.Background(), "GET", "/files/test.txt", nil)
reqCtx, cancel := context.WithCancel(req.Context())
req = req.WithContext(reqCtx)
cancel() // Cancel the request context
@@ -352,7 +353,9 @@ func TestCreateLocalFileStorage(t *testing.T) {
// Create temporary directory for testing
tempDir, err := os.MkdirTemp("", "storage_test")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
defer func() {
require.NoError(t, os.RemoveAll(tempDir))
}()
logger := zap.NewNop()
cfg := config.LocalFSSConfig{
@@ -372,10 +375,12 @@ func TestCreateLocalFileStorage_InvalidPath(t *testing.T) {
// Build a deterministic failure case: the target path already exists as a file.
tempDir, err := os.MkdirTemp("", "storage_invalid_path_test")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
defer func() {
require.NoError(t, os.RemoveAll(tempDir))
}()
fileAtTargetPath := filepath.Join(tempDir, "test")
err = os.WriteFile(fileAtTargetPath, []byte("not a directory"), 0o644)
err = os.WriteFile(fileAtTargetPath, []byte("not a directory"), 0o600)
require.NoError(t, err)
logger := zap.NewNop()
@@ -426,7 +431,7 @@ func TestLocalStorage_ConcurrentOperations(t *testing.T) {
// Create files to delete
for i := 0; i < 5; i++ {
filePath := filepath.Join(tempDir, fmt.Sprintf("delete_%d.txt", i))
err := os.WriteFile(filePath, []byte("content"), 0o644)
err := os.WriteFile(filePath, []byte("content"), 0o600)
require.NoError(t, err)
}
@@ -536,7 +541,7 @@ func BenchmarkLocalStorage_Delete(b *testing.B) {
// Pre-create files for deletion
for i := 0; i < b.N; i++ {
filePath := filepath.Join(tempDir, fmt.Sprintf("bench_delete_%d.txt", i))
err := os.WriteFile(filePath, []byte("content"), 0o644)
err := os.WriteFile(filePath, []byte("content"), 0o600)
if err != nil {
b.Fatal(err)
}

View File

@@ -97,7 +97,9 @@ func (a *LedgerAPI) createAccount(r *http.Request, account *model.Account, token
}
func decodeLedgerAccountCreatePayload(r *http.Request) (*srequest.CreateLedgerAccount, error) {
defer r.Body.Close()
defer func() {
_ = r.Body.Close()
}()
payload := srequest.CreateLedgerAccount{}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {

View File

@@ -46,7 +46,7 @@ func (a *ProtectedAPI[T]) archive(r *http.Request, account *model.Account, acces
ctx := r.Context()
_, err = a.a.DBFactory().TransactionFactory().CreateTransaction().Execute(ctx, func(ctx context.Context) (any, error) {
return nil, a.DB.SetArchived(r.Context(), *account.GetID(), organizationRef, objectRef, *archived, *cascade)
return nil, a.DB.SetArchived(ctx, *account.GetID(), organizationRef, objectRef, *archived, *cascade)
})
if err != nil {
a.Logger.Warn("Failed to change archive property", zap.Error(err), mzap.StorableRef(account), mutil.PLog(a.Cph, r),

View File

@@ -69,7 +69,7 @@ func (a *PaymentAPI) getOperationDocument(r *http.Request, account *model.Accoun
op, err := a.fetchGatewayOperation(r.Context(), gateway.InvokeURI, operationRef)
if err != nil {
a.logger.Warn("Failed to fetch gateway operation for document generation", zap.Error(err), mzap.ObjRef("organization_ref", orgRef), zap.String("gateway_service", string(gatewayService)), zap.String("operation_ref", operationRef))
a.logger.Warn("Failed to fetch gateway operation for document generation", zap.Error(err), mzap.ObjRef("organization_ref", orgRef), zap.String("gateway_service", gatewayService), zap.String("operation_ref", operationRef))
return documentErrorResponse(a.logger, a.Name(), err)
}
@@ -77,7 +77,7 @@ func (a *PaymentAPI) getOperationDocument(r *http.Request, account *model.Accoun
docResp, err := a.fetchOperationDocument(r.Context(), service.InvokeURI, req)
if err != nil {
a.logger.Warn("Failed to fetch operation document", zap.Error(err), mzap.ObjRef("organization_ref", orgRef), zap.String("gateway_service", string(gatewayService)), zap.String("operation_ref", operationRef))
a.logger.Warn("Failed to fetch operation document", zap.Error(err), mzap.ObjRef("organization_ref", orgRef), zap.String("gateway_service", gatewayService), zap.String("operation_ref", operationRef))
return documentErrorResponse(a.logger, a.Name(), err)
}
@@ -154,6 +154,7 @@ func operationDocumentResponse(logger mlogger.Logger, source mservice.Type, docR
w.Header().Set("Content-Type", mimeType)
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
w.WriteHeader(http.StatusOK)
//nolint:gosec // Binary payload is served as attachment with explicit content type.
if _, err := w.Write(docResp.GetContent()); err != nil {
logger.Warn("Failed to write document response", zap.Error(err))
}
@@ -167,15 +168,15 @@ func normalizeGatewayService(raw string) mservice.Type {
}
switch value {
case string(mservice.ChainGateway):
case mservice.ChainGateway:
return mservice.ChainGateway
case string(mservice.TronGateway):
case mservice.TronGateway:
return mservice.TronGateway
case string(mservice.MntxGateway):
case mservice.MntxGateway:
return mservice.MntxGateway
case string(mservice.PaymentGateway):
case mservice.PaymentGateway:
return mservice.PaymentGateway
case string(mservice.TgSettle):
case mservice.TgSettle:
return mservice.TgSettle
default:
return ""
@@ -219,7 +220,11 @@ func (a *PaymentAPI) fetchOperationDocument(ctx context.Context, invokeURI strin
if err != nil {
return nil, merrors.InternalWrap(err, "dial billing documents")
}
defer conn.Close()
defer func() {
if closeErr := conn.Close(); closeErr != nil {
a.logger.Warn("Failed to close billing documents connection", zap.Error(closeErr))
}
}()
client := documentsv1.NewDocumentServiceClient(conn)
@@ -234,7 +239,11 @@ func (a *PaymentAPI) fetchGatewayOperation(ctx context.Context, invokeURI, opera
if err != nil {
return nil, merrors.InternalWrap(err, "dial gateway connector")
}
defer conn.Close()
defer func() {
if closeErr := conn.Close(); closeErr != nil {
a.logger.Warn("Failed to close gateway connector connection", zap.Error(closeErr))
}
}()
client := connectorv1.NewConnectorServiceClient(conn)
@@ -307,7 +316,7 @@ func findGatewayForService(gateways []discovery.GatewaySummary, gatewayService m
func operationDocumentRequest(organizationRef string, gatewayService mservice.Type, requestedOperationRef string, op *connectorv1.Operation) *documentsv1.GetOperationDocumentRequest {
req := &documentsv1.GetOperationDocumentRequest{
OrganizationRef: strings.TrimSpace(organizationRef),
GatewayService: string(gatewayService),
GatewayService: gatewayService,
OperationRef: firstNonEmpty(strings.TrimSpace(op.GetOperationRef()), strings.TrimSpace(requestedOperationRef)),
OperationCode: strings.TrimSpace(op.GetType().String()),
OperationLabel: operationLabel(op.GetType()),

View File

@@ -103,6 +103,7 @@ func listPaymentsPage(r *http.Request) (*paginationv1.CursorPageRequest, error)
}
if cursor == "" && !hasLimit {
//nolint:nilnil // Absent pagination params mean no page request should be sent downstream.
return nil, nil
}
@@ -189,6 +190,7 @@ func firstNonEmpty(values ...string) string {
func parseRFC3339Timestamp(raw string, field string) (*timestamppb.Timestamp, error) {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
//nolint:nilnil // Empty timestamp filter is represented as (nil, nil).
return nil, nil
}
parsed, err := time.Parse(time.RFC3339, trimmed)

View File

@@ -102,7 +102,9 @@ func (a *PaymentAPI) initiatePayment(r *http.Request, account *model.Account, to
}
func decodeInitiatePayload(r *http.Request) (*srequest.InitiatePayment, error) {
defer r.Body.Close()
defer func() {
_ = r.Body.Close()
}()
payload := &srequest.InitiatePayment{}
if err := json.NewDecoder(r.Body).Decode(payload); err != nil {

View File

@@ -68,7 +68,7 @@ func TestInitiateByQuote_RejectsMetadataIntentRef(t *testing.T) {
func invokeInitiateByQuote(t *testing.T, api *PaymentAPI, orgRef bson.ObjectID, body string) *httptest.ResponseRecorder {
t.Helper()
req := httptest.NewRequest(http.MethodPost, "/by-quote", bytes.NewBufferString(body))
req := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/by-quote", bytes.NewBufferString(body))
routeCtx := chi.NewRouteContext()
routeCtx.URLParams.Add("organizations_ref", orgRef.Hex())
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, routeCtx))

View File

@@ -63,7 +63,9 @@ func (a *PaymentAPI) initiatePaymentsByQuote(r *http.Request, account *model.Acc
}
func decodeInitiatePaymentsPayload(r *http.Request) (*srequest.InitiatePayments, error) {
defer r.Body.Close()
defer func() {
_ = r.Body.Close()
}()
payload := &srequest.InitiatePayments{}
decoder := json.NewDecoder(r.Body)

View File

@@ -10,7 +10,6 @@ import (
"github.com/go-chi/chi/v5"
"github.com/tech/sendico/pkg/auth"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
@@ -118,7 +117,7 @@ func TestInitiatePaymentsByQuote_RejectsDeprecatedIntentRefsField(t *testing.T)
func newBatchAPI(exec executionClient) *PaymentAPI {
return &PaymentAPI{
logger: mlogger.Logger(zap.NewNop()),
logger: zap.NewNop(),
execution: exec,
enf: fakeEnforcerForBatch{allowed: true},
oph: mutil.CreatePH(mservice.Organizations),
@@ -129,7 +128,7 @@ func newBatchAPI(exec executionClient) *PaymentAPI {
func invokeInitiatePaymentsByQuote(t *testing.T, api *PaymentAPI, orgRef bson.ObjectID, body string) *httptest.ResponseRecorder {
t.Helper()
req := httptest.NewRequest(http.MethodPost, "/by-multiquote", bytes.NewBufferString(body))
req := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/by-multiquote", bytes.NewBufferString(body))
routeCtx := chi.NewRouteContext()
routeCtx.URLParams.Add("organizations_ref", orgRef.Hex())
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, routeCtx))
@@ -177,6 +176,7 @@ func (f fakeEnforcerForBatch) Enforce(context.Context, bson.ObjectID, bson.Objec
}
func (fakeEnforcerForBatch) EnforceBatch(context.Context, []model.PermissionBoundStorable, bson.ObjectID, model.Action) (map[bson.ObjectID]bool, error) {
//nolint:nilnil // Test stub does not provide batch permissions map.
return nil, nil
}

View File

@@ -126,7 +126,9 @@ func (a *PaymentAPI) quotePayments(r *http.Request, account *model.Account, toke
}
func decodeQuotePayload(r *http.Request) (*srequest.QuotePayment, error) {
defer r.Body.Close()
defer func() {
_ = r.Body.Close()
}()
payload := &srequest.QuotePayment{}
if err := json.NewDecoder(r.Body).Decode(payload); err != nil {
@@ -140,7 +142,9 @@ func decodeQuotePayload(r *http.Request) (*srequest.QuotePayment, error) {
}
func decodeQuotePaymentsPayload(r *http.Request) (*srequest.QuotePayments, error) {
defer r.Body.Close()
defer func() {
_ = r.Body.Close()
}()
payload := &srequest.QuotePayments{}
if err := json.NewDecoder(r.Body).Decode(payload); err != nil {

View File

@@ -256,6 +256,7 @@ func (c *grpcQuotationClient) callContext(ctx context.Context) (context.Context,
if timeout <= 0 {
timeout = 3 * time.Second
}
//nolint:gosec // Caller receives cancel func and defers it in every call path.
return context.WithTimeout(ctx, timeout)
}
@@ -271,7 +272,7 @@ func (a *PaymentAPI) initDiscoveryClient(cfg *eapi.Config) error {
if err != nil {
return err
}
client, err := discovery.NewClient(a.logger, broker, nil, string(a.Name()))
client, err := discovery.NewClient(a.logger, broker, nil, a.Name())
if err != nil {
return err
}

View File

@@ -90,5 +90,5 @@ func (a *PermissionsAPI) changePoliciesImp(
}
}
return nil, nil
return struct{}{}, nil
}

View File

@@ -223,6 +223,7 @@ func (a *WalletAPI) queryBalanceFromGateways(ctx context.Context, gateways []dis
a.logger.Debug("Wallet balance fan-out completed without result",
zap.String("organization_ref", organizationRef),
zap.String("wallet_ref", walletRef))
//nolint:nilnil // No gateway returned a balance and no hard error occurred.
return nil, nil
}
@@ -238,7 +239,11 @@ func (a *WalletAPI) queryGatewayBalance(ctx context.Context, gateway discovery.G
if err != nil {
return nil, merrors.InternalWrap(err, "dial gateway")
}
defer conn.Close()
defer func() {
if closeErr := conn.Close(); closeErr != nil {
a.logger.Warn("Failed to close gateway connection", zap.Error(closeErr), zap.String("gateway", gateway.ID))
}
}()
client := connectorv1.NewConnectorServiceClient(conn)

View File

@@ -173,7 +173,11 @@ func (a *WalletAPI) createWalletOnGateway(ctx context.Context, gateway discovery
if err != nil {
return "", merrors.InternalWrap(err, "dial gateway")
}
defer conn.Close()
defer func() {
if closeErr := conn.Close(); closeErr != nil {
a.logger.Warn("Failed to close gateway connection", zap.Error(closeErr), zap.String("gateway", gateway.ID))
}
}()
client := connectorv1.NewConnectorServiceClient(conn)

View File

@@ -226,7 +226,11 @@ func (a *WalletAPI) queryGateway(ctx context.Context, gateway discovery.GatewayS
if err != nil {
return nil, merrors.InternalWrap(err, "dial gateway")
}
defer conn.Close()
defer func() {
if closeErr := conn.Close(); closeErr != nil {
a.logger.Warn("Failed to close gateway connection", zap.Error(closeErr), zap.String("gateway", gateway.ID))
}
}()
client := connectorv1.NewConnectorServiceClient(conn)

View File

@@ -64,18 +64,21 @@ func (a *WalletAPI) rememberWalletRoute(ctx context.Context, organizationRef str
func (a *WalletAPI) walletRoute(ctx context.Context, organizationRef string, walletRef string) (*model.ChainWalletRoute, error) {
if a.routes == nil {
//nolint:nilnil // Routing cache is optional and may be disabled.
return nil, nil
}
walletRef = strings.TrimSpace(walletRef)
organizationRef = strings.TrimSpace(organizationRef)
if walletRef == "" || organizationRef == "" {
//nolint:nilnil // Missing route keys mean no cached route.
return nil, nil
}
route, err := a.routes.Get(ctx, organizationRef, walletRef)
if err != nil {
if errors.Is(err, merrors.ErrNoData) {
//nolint:nilnil // Route not found in cache.
return nil, nil
}
return nil, err

View File

@@ -129,7 +129,7 @@ func (a *WalletAPI) initDiscoveryClient(cfg *eapi.Config) error {
if err != nil {
return err
}
client, err := discovery.NewClient(a.logger, broker, nil, string(a.Name()))
client, err := discovery.NewClient(a.logger, broker, nil, a.Name())
if err != nil {
return err
}

View File

@@ -9,8 +9,8 @@ import (
)
// generate translations
// go:generate Users/stephandeshevikh/go/bin/go18n extract
// go:generate Users/stephandeshevikh/go/bin/go18n merge
//go:generate Users/stephandeshevikh/go/bin/go18n extract
//go:generate Users/stephandeshevikh/go/bin/go18n merge
// lint go code
// docker run -t --rm -v $(pwd):/app -w /app golangci/golangci-lint:latest golangci-lint run -v --timeout 10m0s --enable-all -D ireturn -D wrapcheck -D varnamelen -D tagliatelle -D nosnakecase -D gochecknoglobals -D nlreturn -D stylecheck -D lll -D wsl -D scopelint -D varcheck -D exhaustivestruct -D golint -D maligned -D interfacer -D ifshort -D structcheck -D deadcode -D godot -D depguard -D tagalign

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

@@ -28,6 +28,7 @@ func (s *service) Load(path string) (*Config, error) {
return nil, merrors.InvalidArgument("config path is required", "path")
}
//nolint:gosec // Configuration file path is provided by service startup configuration.
data, err := os.ReadFile(path)
if err != nil {
s.logger.Error("Failed to read config file", zap.String("path", path), zap.Error(err))

View File

@@ -108,7 +108,7 @@ func (s *service) Start(ctx context.Context) {
if runCtx == nil {
runCtx = context.Background()
}
runCtx, s.cancel = context.WithCancel(runCtx)
runCtx, s.cancel = context.WithCancel(runCtx) //nolint:gosec // canceled by Stop; service lifecycle outlives Start scope
for i := 0; i < s.cfg.WorkerConcurrency; i++ {
workerID := "worker-" + strconv.Itoa(i+1)
@@ -143,6 +143,10 @@ func (s *service) runWorker(ctx context.Context, workerID string) {
now := time.Now().UTC()
task, err := s.tasks.LockNextTask(ctx, now, workerID, s.cfg.LockTTL)
if err != nil {
if errors.Is(err, merrors.ErrNoData) {
time.Sleep(s.cfg.WorkerPoll)
continue
}
s.logger.Warn("Failed to lock next task", zap.String("worker_id", workerID), zap.Error(err))
time.Sleep(s.cfg.WorkerPoll)
continue

View File

@@ -106,7 +106,7 @@ func (s *service) Start(ctx context.Context) {
if runCtx == nil {
runCtx = context.Background()
}
runCtx, s.cancel = context.WithCancel(runCtx)
runCtx, s.cancel = context.WithCancel(runCtx) //nolint:gosec // canceled by Stop; service lifecycle outlives Start scope
s.wg.Add(1)
go func() {

View File

@@ -14,6 +14,7 @@ type service struct {
// New creates retry policy service.
func New() Policy {
//nolint:gosec // Backoff jitter is non-cryptographic and only needs pseudo-random distribution.
return &service{rnd: rand.New(rand.NewSource(time.Now().UnixNano()))}
}

View File

@@ -154,6 +154,7 @@ func (i *Imp) Start() error {
runCtx, cancel := context.WithCancel(context.Background())
i.runCancel = cancel
defer cancel()
i.ingest.Start(runCtx)
i.delivery.Start(runCtx)
i.opServer.SetStatus(health.SSRunning)

View File

@@ -379,7 +379,7 @@ func (r *taskStore) LockNextTask(ctx context.Context, now time.Time, workerID st
candidates, err := mutil.GetObjects[taskDoc](ctx, r.logger, query, nil, r.repo)
if err != nil {
if errors.Is(err, merrors.ErrNoData) {
return nil, nil
return nil, merrors.ErrNoData
}
return nil, merrors.InternalWrap(err, "callbacks task query failed")
}
@@ -418,7 +418,7 @@ func (r *taskStore) LockNextTask(ctx context.Context, now time.Time, workerID st
return mapTaskDoc(locked), nil
}
return nil, nil
return nil, merrors.ErrNoData
}
func (r *taskStore) MarkDelivered(ctx context.Context, taskID bson.ObjectID, httpCode int, latency time.Duration, at time.Time) error {