From 1582347204af5d7607c9b5c095f2efb417bed259 Mon Sep 17 00:00:00 2001 From: "chihao.ou" Date: Sun, 20 Sep 2026 18:05:09 +0800 Subject: [PATCH] fix(payment): trim callback base URL trailing slashes --- .../payment/PaymentProviderDialog.vue | 4 +-- .../__tests__/PaymentProviderDialog.spec.ts | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/payment/PaymentProviderDialog.vue b/frontend/src/components/payment/PaymentProviderDialog.vue index 7743e4073a..f8b1ba6afb 100644 --- a/frontend/src/components/payment/PaymentProviderDialog.vue +++ b/frontend/src/components/payment/PaymentProviderDialog.vue @@ -705,8 +705,8 @@ function handleSave() { // If base URL is empty, auto-fill with current domain const paths = PROVIDER_CALLBACK_PATHS[form.provider_key] if (paths) { - const notifyBase = notifyBaseUrl.value.trim() || defaultBaseUrl - const returnBase = returnBaseUrl.value.trim() || defaultBaseUrl + const notifyBase = (notifyBaseUrl.value.trim() || defaultBaseUrl).replace(/\/+$/, '') + const returnBase = (returnBaseUrl.value.trim() || defaultBaseUrl).replace(/\/+$/, '') notifyBaseUrl.value = notifyBase returnBaseUrl.value = returnBase if (paths.notifyUrl) filteredConfig['notifyUrl'] = notifyBase + paths.notifyUrl diff --git a/frontend/src/components/payment/__tests__/PaymentProviderDialog.spec.ts b/frontend/src/components/payment/__tests__/PaymentProviderDialog.spec.ts index 18ca4cb483..0126faab04 100644 --- a/frontend/src/components/payment/__tests__/PaymentProviderDialog.spec.ts +++ b/frontend/src/components/payment/__tests__/PaymentProviderDialog.spec.ts @@ -95,6 +95,32 @@ function mountDialog(options: { editing?: ProviderInstance | null } = {}) { }) } +describe('PaymentProviderDialog callback URLs', () => { + it.each([ + ['https://notify.example.com/', 'https://return.example.com///', 'https://notify.example.com', 'https://return.example.com'], + [' https://notify.example.com/sub/ ', ' https://return.example.com/site/ ', 'https://notify.example.com/sub', 'https://return.example.com/site'], + ['https://notify.example.com', 'https://return.example.com', 'https://notify.example.com', 'https://return.example.com'], + ['', '', window.location.origin, window.location.origin], + ])('joins callback paths to %s and %s', async (notify, returnUrl, expectedNotify, expectedReturn) => { + const provider = providerFactory({ + provider_key: 'easypay', name: 'EasyPay', + config: { pid: 'pid-1', apiBase: 'https://pay.example.com' }, + supported_types: ['alipay'], payment_mode: 'qrcode', + }) + const wrapper = mountDialog({ editing: provider }) + ;(wrapper.vm as unknown as { loadProvider: (provider: ProviderInstance) => void }).loadProvider(provider) + await nextTick() + const bases = wrapper.findAll('input').filter(input => input.classes().includes('!rounded-r-none')) + await bases[0].setValue(notify) + await bases[1].setValue(returnUrl) + await wrapper.find('form').trigger('submit') + const payload = wrapper.emitted('save')?.[0]?.[0] as { config: Record } + expect(payload.config.notifyUrl).toBe(expectedNotify + '/api/v1/payment/webhook/easypay') + expect(payload.config.returnUrl).toBe(expectedReturn + '/payment/result') + wrapper.unmount() + }) +}) + describe('PaymentProviderDialog payment guide', () => { it('shows no payment guide for providers without a flow guide', () => { const wrapper = mountDialog()