fix: harden easypay custom method validation

This commit is contained in:
Albert Coady
2026-07-06 15:00:29 +08:00
parent bf76168ba5
commit 0dc6e56aae
4 changed files with 108 additions and 4 deletions
@@ -226,7 +226,7 @@ func validateProviderRequest(providerKey, name, supportedTypes string) error {
var easyPayCustomMethodCodePattern = regexp.MustCompile(`^[a-z0-9_-]+$`)
type easyPayCustomMethodConfig struct {
Type string `json:"type"`
Type string `json:"type"`
UpstreamType string `json:"upstreamType"`
DisplayName string `json:"displayName"`
}
@@ -245,8 +245,8 @@ func validateEasyPayCustomMethods(config map[string]string, supportedTypes strin
customTypes := make(map[string]struct{}, len(methods))
for _, method := range methods {
method.Type = strings.TrimSpace(strings.ToLower(method.Type))
method.UpstreamType = strings.TrimSpace(strings.ToLower(method.UpstreamType))
method.Type = strings.TrimSpace(method.Type)
method.UpstreamType = strings.TrimSpace(method.UpstreamType)
if method.Type == "" || method.UpstreamType == "" {
return infraerrors.BadRequest("VALIDATION_ERROR", "customMethods upstreamType is required")
}
@@ -266,10 +266,13 @@ func validateEasyPayCustomMethods(config map[string]string, supportedTypes strin
}
for _, supportedType := range splitTypes(supportedTypes) {
supportedType = strings.TrimSpace(strings.ToLower(supportedType))
supportedType = strings.TrimSpace(supportedType)
if supportedType == "" || supportedType == payment.TypeAlipay || supportedType == payment.TypeWxpay {
continue
}
if !easyPayCustomMethodCodePattern.MatchString(supportedType) {
return infraerrors.BadRequest("VALIDATION_ERROR", fmt.Sprintf("supported EasyPay custom type %s may only contain lowercase letters, digits, underscores, and hyphens", supportedType))
}
if _, exists := customTypes[supportedType]; !exists {
return infraerrors.BadRequest("VALIDATION_ERROR", fmt.Sprintf("supported EasyPay custom type %s has no customMethods mapping", supportedType))
}
@@ -146,6 +146,18 @@ func TestValidateEasyPayCustomMethods(t *testing.T) {
supportedTypes: "alipay,wxpay,ldc",
wantErr: "duplicate customMethods type",
},
{
name: "custom type must already be lowercase",
config: map[string]string{"customMethods": `[{"type":"LDC","upstreamType":"epay"}]`},
supportedTypes: "alipay,wxpay,ldc",
wantErr: "customMethods type may only contain lowercase letters",
},
{
name: "upstream type must already be lowercase",
config: map[string]string{"customMethods": `[{"type":"ldc","upstreamType":"ALIPAY"}]`},
supportedTypes: "alipay,wxpay,ldc",
wantErr: "customMethods upstreamType may only contain lowercase letters",
},
{
name: "custom type uses alipay prefix",
config: map[string]string{"customMethods": `[{"type":"alipay_hk","upstreamType":"hkpay"}]`},
@@ -164,6 +176,12 @@ func TestValidateEasyPayCustomMethods(t *testing.T) {
supportedTypes: "alipay,wxpay,ldc,usdt_trc20",
wantErr: "supported EasyPay custom type usdt_trc20 has no customMethods mapping",
},
{
name: "supported custom type must already be lowercase",
config: map[string]string{"customMethods": `[{"type":"ldc","upstreamType":"epay"}]`},
supportedTypes: "alipay,wxpay,LDC",
wantErr: "supported EasyPay custom type LDC may only contain lowercase letters",
},
}
for _, tc := range tests {
+1
View File
@@ -1119,6 +1119,7 @@ onMounted(async () => {
paymentState.value = restored
paymentPhase.value = 'paying'
const restoredMethod = normalizeVisibleMethod(restored.paymentType)
|| (visibleMethods.value[restored.paymentType] ? restored.paymentType : '')
if (restoredMethod) {
selectedMethod.value = restoredMethod
}
@@ -326,6 +326,88 @@ describe('PaymentView subscription confirmation amounts', () => {
})
})
describe('PaymentView payment recovery', () => {
beforeEach(() => {
vi.useRealTimers()
routeState.path = '/purchase'
routeState.query = {}
routerReplace.mockReset().mockResolvedValue(undefined)
routerPush.mockReset().mockResolvedValue(undefined)
routerResolve.mockClear()
createOrder.mockReset()
refreshUser.mockReset()
fetchActiveSubscriptions.mockReset().mockResolvedValue(undefined)
showError.mockReset()
showInfo.mockReset()
showWarning.mockReset()
bridgeInvoke.mockReset()
window.localStorage.clear()
;(window as Window & { WeixinJSBridge?: { invoke: typeof bridgeInvoke } }).WeixinJSBridge = undefined
})
it('restores a custom EasyPay method as the selected payment method', async () => {
getCheckoutInfo.mockResolvedValue(checkoutInfoFixture({
methods: {
wxpay: checkoutInfoFixture().data.methods.wxpay,
ldc: {
daily_limit: 0,
daily_used: 0,
daily_remaining: 0,
single_min: 0,
single_max: 0,
fee_rate: 0,
available: true,
display_name: 'LDC Pay',
},
},
}))
window.localStorage.setItem(PAYMENT_RECOVERY_STORAGE_KEY, JSON.stringify({
orderId: 888,
amount: 66,
qrCode: 'ldc-qr',
expiresAt: '2099-01-01T00:10:00.000Z',
paymentType: 'ldc',
payUrl: 'https://pay.example.com/ldc',
outTradeNo: 'sub2_ldc_888',
clientSecret: '',
intentId: '',
currency: '',
countryCode: '',
paymentEnv: '',
payAmount: 66,
orderType: 'balance',
paymentMode: 'popup',
resumeToken: '',
createdAt: Date.now(),
}))
const wrapper = shallowMount(PaymentView, {
global: {
stubs: {
AppLayout: {
template: '<div><slot /></div>',
},
PaymentStatusPanel: {
template: '<button data-test="payment-done" @click="$emit(\'done\')" />',
},
PaymentMethodSelector: {
props: ['selected'],
template: '<div data-test="method-selector">{{ selected }}</div>',
},
Teleport: true,
Transition: false,
},
},
})
await flushPromises()
await flushPromises()
await wrapper.find('[data-test="payment-done"]').trigger('click')
await flushPromises()
expect(wrapper.find('[data-test="method-selector"]').text()).toBe('ldc')
})
})
describe('PaymentView WeChat JSAPI flow', () => {
beforeEach(() => {
routeState.path = '/purchase'