mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-02 00:30:32 +00:00
fix: harden easypay custom method validation
This commit is contained in:
@@ -226,7 +226,7 @@ func validateProviderRequest(providerKey, name, supportedTypes string) error {
|
|||||||
var easyPayCustomMethodCodePattern = regexp.MustCompile(`^[a-z0-9_-]+$`)
|
var easyPayCustomMethodCodePattern = regexp.MustCompile(`^[a-z0-9_-]+$`)
|
||||||
|
|
||||||
type easyPayCustomMethodConfig struct {
|
type easyPayCustomMethodConfig struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
UpstreamType string `json:"upstreamType"`
|
UpstreamType string `json:"upstreamType"`
|
||||||
DisplayName string `json:"displayName"`
|
DisplayName string `json:"displayName"`
|
||||||
}
|
}
|
||||||
@@ -245,8 +245,8 @@ func validateEasyPayCustomMethods(config map[string]string, supportedTypes strin
|
|||||||
|
|
||||||
customTypes := make(map[string]struct{}, len(methods))
|
customTypes := make(map[string]struct{}, len(methods))
|
||||||
for _, method := range methods {
|
for _, method := range methods {
|
||||||
method.Type = strings.TrimSpace(strings.ToLower(method.Type))
|
method.Type = strings.TrimSpace(method.Type)
|
||||||
method.UpstreamType = strings.TrimSpace(strings.ToLower(method.UpstreamType))
|
method.UpstreamType = strings.TrimSpace(method.UpstreamType)
|
||||||
if method.Type == "" || method.UpstreamType == "" {
|
if method.Type == "" || method.UpstreamType == "" {
|
||||||
return infraerrors.BadRequest("VALIDATION_ERROR", "customMethods upstreamType is required")
|
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) {
|
for _, supportedType := range splitTypes(supportedTypes) {
|
||||||
supportedType = strings.TrimSpace(strings.ToLower(supportedType))
|
supportedType = strings.TrimSpace(supportedType)
|
||||||
if supportedType == "" || supportedType == payment.TypeAlipay || supportedType == payment.TypeWxpay {
|
if supportedType == "" || supportedType == payment.TypeAlipay || supportedType == payment.TypeWxpay {
|
||||||
continue
|
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 {
|
if _, exists := customTypes[supportedType]; !exists {
|
||||||
return infraerrors.BadRequest("VALIDATION_ERROR", fmt.Sprintf("supported EasyPay custom type %s has no customMethods mapping", supportedType))
|
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",
|
supportedTypes: "alipay,wxpay,ldc",
|
||||||
wantErr: "duplicate customMethods type",
|
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",
|
name: "custom type uses alipay prefix",
|
||||||
config: map[string]string{"customMethods": `[{"type":"alipay_hk","upstreamType":"hkpay"}]`},
|
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",
|
supportedTypes: "alipay,wxpay,ldc,usdt_trc20",
|
||||||
wantErr: "supported EasyPay custom type usdt_trc20 has no customMethods mapping",
|
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 {
|
for _, tc := range tests {
|
||||||
|
|||||||
@@ -1119,6 +1119,7 @@ onMounted(async () => {
|
|||||||
paymentState.value = restored
|
paymentState.value = restored
|
||||||
paymentPhase.value = 'paying'
|
paymentPhase.value = 'paying'
|
||||||
const restoredMethod = normalizeVisibleMethod(restored.paymentType)
|
const restoredMethod = normalizeVisibleMethod(restored.paymentType)
|
||||||
|
|| (visibleMethods.value[restored.paymentType] ? restored.paymentType : '')
|
||||||
if (restoredMethod) {
|
if (restoredMethod) {
|
||||||
selectedMethod.value = 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', () => {
|
describe('PaymentView WeChat JSAPI flow', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
routeState.path = '/purchase'
|
routeState.path = '/purchase'
|
||||||
|
|||||||
Reference in New Issue
Block a user