Admin GET /api/v1/admin/payment/providers previously returned every
config value — including privateKey / apiV3Key / secretKey etc. —
verbatim. Any future XSS on the admin UI would hand attackers the
full set of production payment credentials, and the plaintext values
sat unnecessarily in browser memory for every operator.
Treat those fields as write-only from the admin surface:
- decryptAndMaskConfig() strips sensitive keys from the GET response.
The authoritative list is an explicit per-provider registry that
mirrors the frontend's PROVIDER_CONFIG_FIELDS sensitive flag:
alipay → privateKey, publicKey, alipayPublicKey
wxpay → privateKey, apiV3Key, publicKey
stripe → secretKey, webhookSecret (publishableKey stays plain)
easypay → pkey
Payment runtime still reads the full config via decryptConfig, so
nothing at the gateway changes.
- mergeConfig() treats an empty value for a sensitive key as "leave
unchanged" — the admin UI omits unchanged secrets so operators can
tweak non-sensitive settings without re-entering credentials.
- Admin dialog (PaymentProviderDialog.vue):
* secret inputs get autocomplete="new-password", data-1p-ignore,
data-lpignore and data-bwignore so password managers do not
offer to save provider credentials
* in edit mode the required-field check skips sensitive fields
(empty is the "keep existing" signal) and the placeholder shows
"leave empty to keep" instead of the default example value
* create mode still requires every non-optional field, including
secrets, since there is nothing to preserve
- Unit test renamed to TestIsSensitiveProviderConfigField, covers
the per-provider registry and specifically asserts that Stripe's
publishableKey is NOT treated as a secret.
199 lines
4.2 KiB
Go
199 lines
4.2 KiB
Go
//go:build unit
|
|
|
|
package service
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestValidateProviderRequest(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
providerKey string
|
|
providerName string
|
|
supportedTypes string
|
|
wantErr bool
|
|
errContains string
|
|
}{
|
|
{
|
|
name: "valid easypay with types",
|
|
providerKey: "easypay",
|
|
providerName: "MyProvider",
|
|
supportedTypes: "alipay,wxpay",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid stripe with empty types",
|
|
providerKey: "stripe",
|
|
providerName: "Stripe Provider",
|
|
supportedTypes: "",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid alipay provider",
|
|
providerKey: "alipay",
|
|
providerName: "Alipay Direct",
|
|
supportedTypes: "alipay",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid wxpay provider",
|
|
providerKey: "wxpay",
|
|
providerName: "WeChat Pay",
|
|
supportedTypes: "wxpay",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "invalid provider key",
|
|
providerKey: "invalid",
|
|
providerName: "Name",
|
|
supportedTypes: "alipay",
|
|
wantErr: true,
|
|
errContains: "invalid provider key",
|
|
},
|
|
{
|
|
name: "empty name",
|
|
providerKey: "easypay",
|
|
providerName: "",
|
|
supportedTypes: "alipay",
|
|
wantErr: true,
|
|
errContains: "provider name is required",
|
|
},
|
|
{
|
|
name: "whitespace-only name",
|
|
providerKey: "easypay",
|
|
providerName: " ",
|
|
supportedTypes: "alipay",
|
|
wantErr: true,
|
|
errContains: "provider name is required",
|
|
},
|
|
{
|
|
name: "tab-only name",
|
|
providerKey: "easypay",
|
|
providerName: "\t",
|
|
supportedTypes: "alipay",
|
|
wantErr: true,
|
|
errContains: "provider name is required",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
err := validateProviderRequest(tc.providerKey, tc.providerName, tc.supportedTypes)
|
|
if tc.wantErr {
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), tc.errContains)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsSensitiveProviderConfigField(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
providerKey string
|
|
field string
|
|
wantSen bool
|
|
}{
|
|
// Stripe: publishableKey is public, only secretKey/webhookSecret are secrets
|
|
{"stripe", "secretKey", true},
|
|
{"stripe", "webhookSecret", true},
|
|
{"stripe", "SecretKey", true}, // case-insensitive
|
|
{"stripe", "publishableKey", false},
|
|
{"stripe", "appId", false},
|
|
|
|
// Alipay
|
|
{"alipay", "privateKey", true},
|
|
{"alipay", "publicKey", true},
|
|
{"alipay", "alipayPublicKey", true},
|
|
{"alipay", "appId", false},
|
|
{"alipay", "notifyUrl", false},
|
|
|
|
// Wxpay
|
|
{"wxpay", "privateKey", true},
|
|
{"wxpay", "apiV3Key", true},
|
|
{"wxpay", "publicKey", true},
|
|
{"wxpay", "publicKeyId", false},
|
|
{"wxpay", "certSerial", false},
|
|
{"wxpay", "mchId", false},
|
|
|
|
// EasyPay
|
|
{"easypay", "pkey", true},
|
|
{"easypay", "pid", false},
|
|
{"easypay", "apiBase", false},
|
|
|
|
// Unknown provider: never sensitive
|
|
{"unknown", "secretKey", false},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
t.Run(tc.providerKey+"/"+tc.field, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got := isSensitiveProviderConfigField(tc.providerKey, tc.field)
|
|
assert.Equal(t, tc.wantSen, got, "isSensitiveProviderConfigField(%q, %q)", tc.providerKey, tc.field)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestJoinTypes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
input []string
|
|
want string
|
|
}{
|
|
{
|
|
name: "multiple types",
|
|
input: []string{"alipay", "wxpay"},
|
|
want: "alipay,wxpay",
|
|
},
|
|
{
|
|
name: "single type",
|
|
input: []string{"stripe"},
|
|
want: "stripe",
|
|
},
|
|
{
|
|
name: "empty slice",
|
|
input: []string{},
|
|
want: "",
|
|
},
|
|
{
|
|
name: "nil slice",
|
|
input: nil,
|
|
want: "",
|
|
},
|
|
{
|
|
name: "three types",
|
|
input: []string{"alipay", "wxpay", "stripe"},
|
|
want: "alipay,wxpay,stripe",
|
|
},
|
|
{
|
|
name: "types with spaces are not trimmed",
|
|
input: []string{" alipay ", " wxpay "},
|
|
want: " alipay , wxpay ",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got := joinTypes(tc.input)
|
|
assert.Equal(t, tc.want, got)
|
|
})
|
|
}
|
|
}
|