quotation service fixed
This commit is contained in:
@@ -10,7 +10,7 @@ func TestStableGatewayID(t *testing.T) {
|
||||
want string
|
||||
}{
|
||||
{name: "prefix and key", prefix: "crypto_rail_gateway", key: " TRON ", want: "crypto_rail_gateway_tron"},
|
||||
{name: "prefix trailing underscore", prefix: "payment_gateway_", key: " PROVIDER_SETTLEMENT ", want: "payment_gateway_provider_settlement"},
|
||||
{name: "prefix trailing underscore", prefix: "payment_gateway_", key: " SETTLEMENT ", want: "payment_gateway_settlement"},
|
||||
{name: "missing key", prefix: "payment_gateway", key: " ", want: "payment_gateway_unknown"},
|
||||
{name: "missing prefix", prefix: " ", key: "TRON", want: "tron"},
|
||||
}
|
||||
@@ -35,7 +35,7 @@ func TestStableCryptoRailGatewayID(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestStablePaymentGatewayID(t *testing.T) {
|
||||
if got, want := StablePaymentGatewayID(" PROVIDER_SETTLEMENT "), "payment_gateway_provider_settlement"; got != want {
|
||||
if got, want := StablePaymentGatewayID(" SETTLEMENT "), "payment_gateway_settlement"; got != want {
|
||||
t.Fatalf("unexpected stable id: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := StablePaymentGatewayID(""), "payment_gateway_unknown"; got != want {
|
||||
|
||||
@@ -4,10 +4,10 @@ import "strings"
|
||||
|
||||
const (
|
||||
RailCrypto = "CRYPTO"
|
||||
RailProviderSettlement = "PROVIDER_SETTLEMENT"
|
||||
RailProviderSettlement = "SETTLEMENT"
|
||||
RailLedger = "LEDGER"
|
||||
RailCardPayout = "CARD_PAYOUT"
|
||||
RailFiatOnRamp = "FIAT_ONRAMP"
|
||||
RailCardPayout = "CARD"
|
||||
RailFiatOnRamp = "ONRAMP"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -48,7 +48,30 @@ var knownRailOperations = map[string]struct{}{
|
||||
|
||||
// NormalizeRail canonicalizes a rail token.
|
||||
func NormalizeRail(value string) string {
|
||||
return strings.ToUpper(strings.TrimSpace(value))
|
||||
clean := strings.ToUpper(strings.TrimSpace(value))
|
||||
if clean == "" {
|
||||
return ""
|
||||
}
|
||||
clean = strings.ReplaceAll(clean, "-", "_")
|
||||
clean = strings.ReplaceAll(clean, " ", "_")
|
||||
for strings.Contains(clean, "__") {
|
||||
clean = strings.ReplaceAll(clean, "__", "_")
|
||||
}
|
||||
|
||||
switch clean {
|
||||
case RailCrypto, "RAIL_CRYPTO":
|
||||
return RailCrypto
|
||||
case RailProviderSettlement, "PROVIDER_SETTLEMENT", "RAIL_SETTLEMENT", "RAIL_PROVIDER_SETTLEMENT":
|
||||
return RailProviderSettlement
|
||||
case RailLedger, "RAIL_LEDGER":
|
||||
return RailLedger
|
||||
case RailCardPayout, "CARD_PAYOUT", "RAIL_CARD", "RAIL_CARD_PAYOUT":
|
||||
return RailCardPayout
|
||||
case RailFiatOnRamp, "FIAT_ONRAMP", "RAIL_ONRAMP", "RAIL_FIAT_ONRAMP":
|
||||
return RailFiatOnRamp
|
||||
default:
|
||||
return clean
|
||||
}
|
||||
}
|
||||
|
||||
// IsKnownRail reports whether the value is a recognized payment rail.
|
||||
@@ -60,8 +83,8 @@ func IsKnownRail(value string) bool {
|
||||
// NormalizeRailOperation canonicalizes a rail operation token.
|
||||
func NormalizeRailOperation(value string) string {
|
||||
clean := strings.ToUpper(strings.TrimSpace(value))
|
||||
if strings.HasPrefix(clean, "RAIL_OPERATION_") {
|
||||
clean = strings.TrimPrefix(clean, "RAIL_OPERATION_")
|
||||
if after, ok := strings.CutPrefix(clean, "RAIL_OPERATION_"); ok {
|
||||
clean = after
|
||||
}
|
||||
return clean
|
||||
}
|
||||
@@ -140,3 +163,11 @@ func CardPayoutRailGatewayOperations() []string {
|
||||
RailOperationObserveConfirm,
|
||||
}
|
||||
}
|
||||
|
||||
// ProviderSettlementRailGatewayOperations returns canonical operations for settlement gateways.
|
||||
func ProviderSettlementRailGatewayOperations() []string {
|
||||
return []string{
|
||||
RailOperationFXConvert,
|
||||
RailOperationObserveConfirm,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,3 +47,15 @@ func TestIsKnownRail(t *testing.T) {
|
||||
t.Fatalf("did not expect telegram rail to be known")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeRailAliases(t *testing.T) {
|
||||
if got := NormalizeRail("provider_settlement"); got != RailProviderSettlement {
|
||||
t.Fatalf("provider_settlement alias mismatch: got=%q want=%q", got, RailProviderSettlement)
|
||||
}
|
||||
if got := NormalizeRail("card_payout"); got != RailCardPayout {
|
||||
t.Fatalf("card_payout alias mismatch: got=%q want=%q", got, RailCardPayout)
|
||||
}
|
||||
if got := NormalizeRail("RAIL_SETTLEMENT"); got != RailProviderSettlement {
|
||||
t.Fatalf("RAIL_SETTLEMENT alias mismatch: got=%q want=%q", got, RailProviderSettlement)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,40 +34,108 @@ type envConfig struct {
|
||||
}
|
||||
|
||||
const defaultConsumerBufferSize = 1024
|
||||
const redactedNATSPassword = "xxxxx"
|
||||
|
||||
func sanitizeNATSURL(rawURL string) string {
|
||||
func buildSafePublishableNATSURL(rawURL string) string {
|
||||
if rawURL == "" {
|
||||
return rawURL
|
||||
}
|
||||
|
||||
parts := strings.Split(rawURL, ",")
|
||||
sanitized := make([]string, 0, len(parts))
|
||||
safe := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
trimmed := strings.TrimSpace(part)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(trimmed, "://") {
|
||||
sanitized = append(sanitized, trimmed)
|
||||
built, ok := buildSafePublishableNATSEntry(trimmed)
|
||||
if !ok {
|
||||
safe = append(safe, trimmed)
|
||||
continue
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(trimmed)
|
||||
if err != nil {
|
||||
sanitized = append(sanitized, trimmed)
|
||||
continue
|
||||
}
|
||||
if parsed.User == nil {
|
||||
sanitized = append(sanitized, trimmed)
|
||||
continue
|
||||
}
|
||||
sanitized = append(sanitized, parsed.Redacted())
|
||||
safe = append(safe, built)
|
||||
}
|
||||
|
||||
if len(sanitized) == 0 {
|
||||
if len(safe) == 0 {
|
||||
return strings.TrimSpace(rawURL)
|
||||
}
|
||||
return strings.Join(sanitized, ",")
|
||||
return strings.Join(safe, ",")
|
||||
}
|
||||
|
||||
func buildSafePublishableNATSEntry(raw string) (string, bool) {
|
||||
parsed, err := url.Parse(raw)
|
||||
if err == nil && parsed.Host != "" {
|
||||
safe := &url.URL{
|
||||
Scheme: parsed.Scheme,
|
||||
Host: parsed.Host,
|
||||
Path: parsed.Path,
|
||||
RawPath: parsed.RawPath,
|
||||
RawQuery: parsed.RawQuery,
|
||||
Fragment: parsed.Fragment,
|
||||
}
|
||||
if safe.Scheme == "" {
|
||||
safe.Scheme = "nats"
|
||||
}
|
||||
if parsed.User != nil {
|
||||
username := parsed.User.Username()
|
||||
if username == "" {
|
||||
username = redactedNATSPassword
|
||||
}
|
||||
safe.User = url.UserPassword(username, redactedNATSPassword)
|
||||
}
|
||||
return safe.String(), true
|
||||
}
|
||||
|
||||
return buildSafePublishableFromAuthority(raw)
|
||||
}
|
||||
|
||||
func buildSafePublishableFromAuthority(raw string) (string, bool) {
|
||||
scheme := "nats"
|
||||
authorityAndSuffix := raw
|
||||
if schemeIndex := strings.Index(raw, "://"); schemeIndex >= 0 {
|
||||
if candidate := strings.TrimSpace(raw[:schemeIndex]); candidate != "" {
|
||||
scheme = candidate
|
||||
}
|
||||
authorityAndSuffix = raw[schemeIndex+3:]
|
||||
}
|
||||
|
||||
authorityEnd := strings.IndexAny(authorityAndSuffix, "/?#")
|
||||
if authorityEnd < 0 {
|
||||
authorityEnd = len(authorityAndSuffix)
|
||||
}
|
||||
|
||||
authority := authorityAndSuffix[:authorityEnd]
|
||||
suffix := authorityAndSuffix[authorityEnd:]
|
||||
atIndex := strings.LastIndex(authority, "@")
|
||||
hostPort := authority
|
||||
username := ""
|
||||
if atIndex >= 0 {
|
||||
userInfo := authority[:atIndex]
|
||||
hostPort = authority[atIndex+1:]
|
||||
if hostPort == "" {
|
||||
return "", false
|
||||
}
|
||||
username = userInfo
|
||||
if colonIndex := strings.Index(userInfo, ":"); colonIndex >= 0 {
|
||||
username = userInfo[:colonIndex]
|
||||
}
|
||||
if username == "" {
|
||||
username = redactedNATSPassword
|
||||
}
|
||||
}
|
||||
|
||||
if hostPort == "" || strings.ContainsAny(hostPort, " \t\r\n") {
|
||||
return "", false
|
||||
}
|
||||
|
||||
safe := &url.URL{
|
||||
Scheme: scheme,
|
||||
Host: hostPort,
|
||||
}
|
||||
if username != "" {
|
||||
safe.User = url.UserPassword(username, redactedNATSPassword)
|
||||
}
|
||||
return safe.String() + suffix, true
|
||||
}
|
||||
|
||||
// loadEnv gathers and validates connection details from environment variables
|
||||
@@ -144,7 +212,7 @@ func NewNatsBroker(logger mlogger.Logger, settings *nc.Settings) (*NatsBroker, e
|
||||
}
|
||||
natsURL = u.String()
|
||||
}
|
||||
sanitizedNATSURL := sanitizeNATSURL(natsURL)
|
||||
publishableNATSURL := buildSafePublishableNATSURL(natsURL)
|
||||
|
||||
opts := []nats.Option{
|
||||
nats.Name(settings.NATSName),
|
||||
@@ -156,7 +224,7 @@ func NewNatsBroker(logger mlogger.Logger, settings *nc.Settings) (*NatsBroker, e
|
||||
zap.String("broker", settings.NATSName),
|
||||
}
|
||||
if conn != nil {
|
||||
fields = append(fields, zap.String("connected_url", sanitizeNATSURL(conn.ConnectedUrl())))
|
||||
fields = append(fields, zap.String("connected_url", buildSafePublishableNATSURL(conn.ConnectedUrl())))
|
||||
}
|
||||
if err != nil {
|
||||
fields = append(fields, zap.Error(err))
|
||||
@@ -168,7 +236,7 @@ func NewNatsBroker(logger mlogger.Logger, settings *nc.Settings) (*NatsBroker, e
|
||||
zap.String("broker", settings.NATSName),
|
||||
}
|
||||
if conn != nil {
|
||||
fields = append(fields, zap.String("connected_url", sanitizeNATSURL(conn.ConnectedUrl())))
|
||||
fields = append(fields, zap.String("connected_url", buildSafePublishableNATSURL(conn.ConnectedUrl())))
|
||||
}
|
||||
l.Info("Reconnected to NATS", fields...)
|
||||
}),
|
||||
@@ -178,7 +246,7 @@ func NewNatsBroker(logger mlogger.Logger, settings *nc.Settings) (*NatsBroker, e
|
||||
}
|
||||
if conn != nil {
|
||||
if url := conn.ConnectedUrl(); url != "" {
|
||||
fields = append(fields, zap.String("connected_url", sanitizeNATSURL(url)))
|
||||
fields = append(fields, zap.String("connected_url", buildSafePublishableNATSURL(url)))
|
||||
}
|
||||
if err := conn.LastError(); err != nil {
|
||||
fields = append(fields, zap.Error(err))
|
||||
@@ -208,7 +276,7 @@ func NewNatsBroker(logger mlogger.Logger, settings *nc.Settings) (*NatsBroker, e
|
||||
}
|
||||
|
||||
if res.nc, err = nats.Connect(natsURL, opts...); err != nil {
|
||||
l.Error("Failed to connect to NATS", zap.String("url", sanitizedNATSURL), zap.Error(err))
|
||||
l.Error("Failed to connect to NATS", zap.String("url", publishableNATSURL), zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
if res.js, err = res.nc.JetStream(); err != nil {
|
||||
@@ -216,7 +284,7 @@ func NewNatsBroker(logger mlogger.Logger, settings *nc.Settings) (*NatsBroker, e
|
||||
}
|
||||
|
||||
logger.Info("Connected to NATS", zap.String("broker", settings.NATSName),
|
||||
zap.String("url", sanitizedNATSURL))
|
||||
zap.String("url", publishableNATSURL))
|
||||
return res, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -5,14 +5,14 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSanitizeNATSURL(t *testing.T) {
|
||||
func TestBuildSafePublishableNATSURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("redacts single URL credentials", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
raw := "nats://alice:supersecret@localhost:4222"
|
||||
sanitized := sanitizeNATSURL(raw)
|
||||
sanitized := buildSafePublishableNATSURL(raw)
|
||||
|
||||
if strings.Contains(sanitized, "supersecret") {
|
||||
t.Fatalf("expected password to be redacted, got %q", sanitized)
|
||||
@@ -22,11 +22,25 @@ func TestSanitizeNATSURL(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("redacts credentials in gateway URL format", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
raw := "nats://dev_nats:nats_password_123@dev-nats:4222"
|
||||
sanitized := buildSafePublishableNATSURL(raw)
|
||||
|
||||
if strings.Contains(sanitized, "nats_password_123") {
|
||||
t.Fatalf("expected password to be redacted, got %q", sanitized)
|
||||
}
|
||||
if !strings.Contains(sanitized, "dev_nats:xxxxx@dev-nats:4222") {
|
||||
t.Fatalf("expected sanitized URL with redacted password, got %q", sanitized)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("keeps URL without credentials unchanged", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
raw := "nats://localhost:4222"
|
||||
sanitized := sanitizeNATSURL(raw)
|
||||
sanitized := buildSafePublishableNATSURL(raw)
|
||||
if sanitized != raw {
|
||||
t.Fatalf("expected URL without credentials to remain unchanged, got %q", sanitized)
|
||||
}
|
||||
@@ -36,7 +50,7 @@ func TestSanitizeNATSURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
raw := " nats://alice:one@localhost:4222, nats://bob:two@localhost:4223 "
|
||||
sanitized := sanitizeNATSURL(raw)
|
||||
sanitized := buildSafePublishableNATSURL(raw)
|
||||
|
||||
if strings.Contains(sanitized, "one") || strings.Contains(sanitized, "two") {
|
||||
t.Fatalf("expected passwords to be redacted, got %q", sanitized)
|
||||
@@ -50,9 +64,37 @@ func TestSanitizeNATSURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
raw := "not a url"
|
||||
sanitized := sanitizeNATSURL(raw)
|
||||
sanitized := buildSafePublishableNATSURL(raw)
|
||||
if sanitized != raw {
|
||||
t.Fatalf("expected invalid URL to remain unchanged, got %q", sanitized)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("redacts malformed URL credentials via fallback", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
raw := "nats://alice:pa%ss@localhost:4222"
|
||||
sanitized := buildSafePublishableNATSURL(raw)
|
||||
|
||||
if strings.Contains(sanitized, "pa%ss") {
|
||||
t.Fatalf("expected malformed password to be redacted, got %q", sanitized)
|
||||
}
|
||||
if !strings.Contains(sanitized, "alice:xxxxx@localhost:4222") {
|
||||
t.Fatalf("expected fallback redaction to preserve host and username, got %q", sanitized)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("redacts URL without scheme when user info is present", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
raw := "alice:topsecret@localhost:4222"
|
||||
sanitized := buildSafePublishableNATSURL(raw)
|
||||
|
||||
if strings.Contains(sanitized, "topsecret") {
|
||||
t.Fatalf("expected password to be redacted, got %q", sanitized)
|
||||
}
|
||||
if !strings.Contains(sanitized, "alice:xxxxx@localhost:4222") {
|
||||
t.Fatalf("expected sanitized authority with redacted password, got %q", sanitized)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user