test(admin): constrain payment visible method sources
This commit is contained in:
@@ -0,0 +1,63 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import {
|
||||||
|
getPaymentVisibleMethodSourceOptions,
|
||||||
|
normalizePaymentVisibleMethodSource,
|
||||||
|
} from '@/api/admin/settings'
|
||||||
|
|
||||||
|
describe('admin settings payment visible method helpers', () => {
|
||||||
|
it('normalizes aliases into canonical source keys per visible method', () => {
|
||||||
|
expect(normalizePaymentVisibleMethodSource('alipay', 'official')).toBe('official_alipay')
|
||||||
|
expect(normalizePaymentVisibleMethodSource('alipay', 'alipay_direct')).toBe('official_alipay')
|
||||||
|
expect(normalizePaymentVisibleMethodSource('alipay', 'easypay')).toBe('easypay_alipay')
|
||||||
|
|
||||||
|
expect(normalizePaymentVisibleMethodSource('wxpay', 'official')).toBe('official_wxpay')
|
||||||
|
expect(normalizePaymentVisibleMethodSource('wxpay', 'wechat')).toBe('official_wxpay')
|
||||||
|
expect(normalizePaymentVisibleMethodSource('wxpay', 'easypay')).toBe('easypay_wxpay')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects unknown or cross-method source values', () => {
|
||||||
|
expect(normalizePaymentVisibleMethodSource('alipay', 'official_wxpay')).toBe('')
|
||||||
|
expect(normalizePaymentVisibleMethodSource('wxpay', 'official_alipay')).toBe('')
|
||||||
|
expect(normalizePaymentVisibleMethodSource('alipay', 'unknown')).toBe('')
|
||||||
|
expect(normalizePaymentVisibleMethodSource('wxpay', null)).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('exposes method-scoped source options instead of arbitrary strings', () => {
|
||||||
|
expect(getPaymentVisibleMethodSourceOptions('alipay')).toEqual([
|
||||||
|
{
|
||||||
|
value: '',
|
||||||
|
labelZh: '自动路由',
|
||||||
|
labelEn: 'Automatic routing',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'official_alipay',
|
||||||
|
labelZh: '支付宝官方',
|
||||||
|
labelEn: 'Official Alipay',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'easypay_alipay',
|
||||||
|
labelZh: '易支付支付宝',
|
||||||
|
labelEn: 'EasyPay Alipay',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(getPaymentVisibleMethodSourceOptions('wxpay')).toEqual([
|
||||||
|
{
|
||||||
|
value: '',
|
||||||
|
labelZh: '自动路由',
|
||||||
|
labelEn: 'Automatic routing',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'official_wxpay',
|
||||||
|
labelZh: '微信官方',
|
||||||
|
labelEn: 'Official WeChat Pay',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'easypay_wxpay',
|
||||||
|
labelZh: '易支付微信',
|
||||||
|
labelEn: 'EasyPay WeChat Pay',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -22,10 +22,60 @@ export interface AuthSourceDefaultsValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type AuthSourceDefaultsState = Record<AuthSourceType, AuthSourceDefaultsValue>
|
export type AuthSourceDefaultsState = Record<AuthSourceType, AuthSourceDefaultsValue>
|
||||||
|
export type PaymentVisibleMethod = 'alipay' | 'wxpay'
|
||||||
|
export type PaymentVisibleMethodSource =
|
||||||
|
| ''
|
||||||
|
| 'official_alipay'
|
||||||
|
| 'easypay_alipay'
|
||||||
|
| 'official_wxpay'
|
||||||
|
| 'easypay_wxpay'
|
||||||
|
|
||||||
|
export interface PaymentVisibleMethodSourceOption {
|
||||||
|
value: PaymentVisibleMethodSource
|
||||||
|
labelZh: string
|
||||||
|
labelEn: string
|
||||||
|
}
|
||||||
|
|
||||||
const AUTH_SOURCE_TYPES: AuthSourceType[] = ['email', 'linuxdo', 'oidc', 'wechat']
|
const AUTH_SOURCE_TYPES: AuthSourceType[] = ['email', 'linuxdo', 'oidc', 'wechat']
|
||||||
const AUTH_SOURCE_DEFAULT_BALANCE = 0
|
const AUTH_SOURCE_DEFAULT_BALANCE = 0
|
||||||
const AUTH_SOURCE_DEFAULT_CONCURRENCY = 5
|
const AUTH_SOURCE_DEFAULT_CONCURRENCY = 5
|
||||||
|
const PAYMENT_VISIBLE_METHOD_SOURCE_OPTIONS: Record<
|
||||||
|
PaymentVisibleMethod,
|
||||||
|
PaymentVisibleMethodSourceOption[]
|
||||||
|
> = {
|
||||||
|
alipay: [
|
||||||
|
{ value: '', labelZh: '自动路由', labelEn: 'Automatic routing' },
|
||||||
|
{ value: 'official_alipay', labelZh: '支付宝官方', labelEn: 'Official Alipay' },
|
||||||
|
{ value: 'easypay_alipay', labelZh: '易支付支付宝', labelEn: 'EasyPay Alipay' },
|
||||||
|
],
|
||||||
|
wxpay: [
|
||||||
|
{ value: '', labelZh: '自动路由', labelEn: 'Automatic routing' },
|
||||||
|
{ value: 'official_wxpay', labelZh: '微信官方', labelEn: 'Official WeChat Pay' },
|
||||||
|
{ value: 'easypay_wxpay', labelZh: '易支付微信', labelEn: 'EasyPay WeChat Pay' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const PAYMENT_VISIBLE_METHOD_SOURCE_ALIASES: Record<
|
||||||
|
PaymentVisibleMethod,
|
||||||
|
Record<string, PaymentVisibleMethodSource>
|
||||||
|
> = {
|
||||||
|
alipay: {
|
||||||
|
official_alipay: 'official_alipay',
|
||||||
|
alipay: 'official_alipay',
|
||||||
|
alipay_direct: 'official_alipay',
|
||||||
|
official: 'official_alipay',
|
||||||
|
easypay_alipay: 'easypay_alipay',
|
||||||
|
easypay: 'easypay_alipay',
|
||||||
|
},
|
||||||
|
wxpay: {
|
||||||
|
official_wxpay: 'official_wxpay',
|
||||||
|
wxpay: 'official_wxpay',
|
||||||
|
wxpay_direct: 'official_wxpay',
|
||||||
|
wechat: 'official_wxpay',
|
||||||
|
official: 'official_wxpay',
|
||||||
|
easypay_wxpay: 'easypay_wxpay',
|
||||||
|
easypay: 'easypay_wxpay',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeDefaultSubscriptionSettings(
|
export function normalizeDefaultSubscriptionSettings(
|
||||||
subscriptions: DefaultSubscriptionSetting[] | null | undefined
|
subscriptions: DefaultSubscriptionSetting[] | null | undefined
|
||||||
@@ -86,6 +136,24 @@ export function appendAuthSourceDefaultsToUpdateRequest(
|
|||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getPaymentVisibleMethodSourceOptions(
|
||||||
|
method: PaymentVisibleMethod
|
||||||
|
): PaymentVisibleMethodSourceOption[] {
|
||||||
|
return PAYMENT_VISIBLE_METHOD_SOURCE_OPTIONS[method]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizePaymentVisibleMethodSource(
|
||||||
|
method: PaymentVisibleMethod,
|
||||||
|
source: unknown
|
||||||
|
): PaymentVisibleMethodSource {
|
||||||
|
if (typeof source !== 'string') return ''
|
||||||
|
|
||||||
|
const normalized = source.trim().toLowerCase()
|
||||||
|
if (!normalized) return ''
|
||||||
|
|
||||||
|
return PAYMENT_VISIBLE_METHOD_SOURCE_ALIASES[method][normalized] ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* System settings interface
|
* System settings interface
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2717,20 +2717,19 @@
|
|||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<label class="input-label">
|
<label class="input-label">
|
||||||
{{ localText('来源键', 'Source key') }}
|
{{ localText('支付来源', 'Payment source') }}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<Select
|
||||||
:value="getPaymentVisibleMethodSource(visibleMethod.key)"
|
:model-value="getPaymentVisibleMethodSource(visibleMethod.key)"
|
||||||
@input="setPaymentVisibleMethodSource(visibleMethod.key, ($event.target as HTMLInputElement).value)"
|
:options="getPaymentVisibleMethodSourceSelectOptions(visibleMethod.key)"
|
||||||
type="text"
|
@update:model-value="setPaymentVisibleMethodSource(visibleMethod.key, $event)"
|
||||||
class="input"
|
|
||||||
:placeholder="visibleMethod.key"
|
:placeholder="visibleMethod.key"
|
||||||
/>
|
/>
|
||||||
<p class="mt-1.5 text-xs text-gray-400">
|
<p class="mt-1.5 text-xs text-gray-400">
|
||||||
{{
|
{{
|
||||||
localText(
|
localText(
|
||||||
'留空表示由后端使用默认来源;可填 easypay、alipay、wxpay 等来源标识。',
|
'留空表示自动路由;仅允许当前系统支持的官方或易支付来源。',
|
||||||
'Leave blank to let the backend decide. Typical values are easypay, alipay, or wxpay.'
|
'Leave blank for automatic routing. Only supported official or EasyPay sources are allowed.'
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
@@ -3117,11 +3116,14 @@ import { adminAPI } from '@/api'
|
|||||||
import {
|
import {
|
||||||
appendAuthSourceDefaultsToUpdateRequest,
|
appendAuthSourceDefaultsToUpdateRequest,
|
||||||
buildAuthSourceDefaultsState,
|
buildAuthSourceDefaultsState,
|
||||||
|
getPaymentVisibleMethodSourceOptions,
|
||||||
|
normalizePaymentVisibleMethodSource,
|
||||||
normalizeDefaultSubscriptionSettings,
|
normalizeDefaultSubscriptionSettings,
|
||||||
} from '@/api/admin/settings'
|
} from '@/api/admin/settings'
|
||||||
import type {
|
import type {
|
||||||
AuthSourceDefaultsState,
|
AuthSourceDefaultsState,
|
||||||
AuthSourceType,
|
AuthSourceType,
|
||||||
|
PaymentVisibleMethod,
|
||||||
SystemSettings,
|
SystemSettings,
|
||||||
UpdateSettingsRequest,
|
UpdateSettingsRequest,
|
||||||
DefaultSubscriptionSetting,
|
DefaultSubscriptionSetting,
|
||||||
@@ -3429,12 +3431,23 @@ function getPaymentVisibleMethodSource(method: 'alipay' | 'wxpay'): string {
|
|||||||
: form.payment_visible_method_wxpay_source
|
: form.payment_visible_method_wxpay_source
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPaymentVisibleMethodSource(method: 'alipay' | 'wxpay', source: string) {
|
function getPaymentVisibleMethodSourceSelectOptions(method: PaymentVisibleMethod) {
|
||||||
|
return getPaymentVisibleMethodSourceOptions(method).map((option) => ({
|
||||||
|
value: option.value,
|
||||||
|
label: localText(option.labelZh, option.labelEn),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPaymentVisibleMethodSource(
|
||||||
|
method: 'alipay' | 'wxpay',
|
||||||
|
source: string | number | boolean | null
|
||||||
|
) {
|
||||||
|
const normalized = normalizePaymentVisibleMethodSource(method, source)
|
||||||
if (method === 'alipay') {
|
if (method === 'alipay') {
|
||||||
form.payment_visible_method_alipay_source = source
|
form.payment_visible_method_alipay_source = normalized
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
form.payment_visible_method_wxpay_source = source
|
form.payment_visible_method_wxpay_source = normalized
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proxies for web search emulation ProxySelector
|
// Proxies for web search emulation ProxySelector
|
||||||
@@ -3805,6 +3818,14 @@ async function loadSettings() {
|
|||||||
Object.assign(authSourceDefaults, buildAuthSourceDefaultsState(settings))
|
Object.assign(authSourceDefaults, buildAuthSourceDefaultsState(settings))
|
||||||
form.backend_mode_enabled = settings.backend_mode_enabled
|
form.backend_mode_enabled = settings.backend_mode_enabled
|
||||||
form.default_subscriptions = normalizeDefaultSubscriptionSettings(settings.default_subscriptions)
|
form.default_subscriptions = normalizeDefaultSubscriptionSettings(settings.default_subscriptions)
|
||||||
|
form.payment_visible_method_alipay_source = normalizePaymentVisibleMethodSource(
|
||||||
|
'alipay',
|
||||||
|
settings.payment_visible_method_alipay_source
|
||||||
|
)
|
||||||
|
form.payment_visible_method_wxpay_source = normalizePaymentVisibleMethodSource(
|
||||||
|
'wxpay',
|
||||||
|
settings.payment_visible_method_wxpay_source
|
||||||
|
)
|
||||||
registrationEmailSuffixWhitelistTags.value = normalizeRegistrationEmailSuffixDomains(
|
registrationEmailSuffixWhitelistTags.value = normalizeRegistrationEmailSuffixDomains(
|
||||||
settings.registration_email_suffix_whitelist
|
settings.registration_email_suffix_whitelist
|
||||||
)
|
)
|
||||||
@@ -4070,8 +4091,14 @@ async function saveSettings() {
|
|||||||
payment_cancel_rate_limit_window: Number(form.payment_cancel_rate_limit_window) || 1,
|
payment_cancel_rate_limit_window: Number(form.payment_cancel_rate_limit_window) || 1,
|
||||||
payment_cancel_rate_limit_unit: form.payment_cancel_rate_limit_unit,
|
payment_cancel_rate_limit_unit: form.payment_cancel_rate_limit_unit,
|
||||||
payment_cancel_rate_limit_window_mode: form.payment_cancel_rate_limit_window_mode,
|
payment_cancel_rate_limit_window_mode: form.payment_cancel_rate_limit_window_mode,
|
||||||
payment_visible_method_alipay_source: form.payment_visible_method_alipay_source,
|
payment_visible_method_alipay_source: normalizePaymentVisibleMethodSource(
|
||||||
payment_visible_method_wxpay_source: form.payment_visible_method_wxpay_source,
|
'alipay',
|
||||||
|
form.payment_visible_method_alipay_source
|
||||||
|
),
|
||||||
|
payment_visible_method_wxpay_source: normalizePaymentVisibleMethodSource(
|
||||||
|
'wxpay',
|
||||||
|
form.payment_visible_method_wxpay_source
|
||||||
|
),
|
||||||
payment_visible_method_alipay_enabled: form.payment_visible_method_alipay_enabled,
|
payment_visible_method_alipay_enabled: form.payment_visible_method_alipay_enabled,
|
||||||
payment_visible_method_wxpay_enabled: form.payment_visible_method_wxpay_enabled,
|
payment_visible_method_wxpay_enabled: form.payment_visible_method_wxpay_enabled,
|
||||||
openai_advanced_scheduler_enabled: form.openai_advanced_scheduler_enabled,
|
openai_advanced_scheduler_enabled: form.openai_advanced_scheduler_enabled,
|
||||||
@@ -4092,6 +4119,14 @@ async function saveSettings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Object.assign(authSourceDefaults, buildAuthSourceDefaultsState(updated))
|
Object.assign(authSourceDefaults, buildAuthSourceDefaultsState(updated))
|
||||||
|
form.payment_visible_method_alipay_source = normalizePaymentVisibleMethodSource(
|
||||||
|
'alipay',
|
||||||
|
updated.payment_visible_method_alipay_source
|
||||||
|
)
|
||||||
|
form.payment_visible_method_wxpay_source = normalizePaymentVisibleMethodSource(
|
||||||
|
'wxpay',
|
||||||
|
updated.payment_visible_method_wxpay_source
|
||||||
|
)
|
||||||
registrationEmailSuffixWhitelistTags.value = normalizeRegistrationEmailSuffixDomains(
|
registrationEmailSuffixWhitelistTags.value = normalizeRegistrationEmailSuffixDomains(
|
||||||
updated.registration_email_suffix_whitelist
|
updated.registration_email_suffix_whitelist
|
||||||
)
|
)
|
||||||
|
|||||||
452
frontend/src/views/admin/__tests__/SettingsView.spec.ts
Normal file
452
frontend/src/views/admin/__tests__/SettingsView.spec.ts
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { defineComponent, h, ref } from 'vue'
|
||||||
|
import { flushPromises, mount } from '@vue/test-utils'
|
||||||
|
|
||||||
|
import SettingsView from '../SettingsView.vue'
|
||||||
|
|
||||||
|
const {
|
||||||
|
getSettings,
|
||||||
|
updateSettings,
|
||||||
|
getWebSearchEmulationConfig,
|
||||||
|
updateWebSearchEmulationConfig,
|
||||||
|
getAdminApiKey,
|
||||||
|
getOverloadCooldownSettings,
|
||||||
|
getStreamTimeoutSettings,
|
||||||
|
getRectifierSettings,
|
||||||
|
getBetaPolicySettings,
|
||||||
|
getGroups,
|
||||||
|
listProxies,
|
||||||
|
getProviders,
|
||||||
|
fetchPublicSettings,
|
||||||
|
adminSettingsFetch,
|
||||||
|
showError,
|
||||||
|
showSuccess,
|
||||||
|
} = vi.hoisted(() => ({
|
||||||
|
getSettings: vi.fn(),
|
||||||
|
updateSettings: vi.fn(),
|
||||||
|
getWebSearchEmulationConfig: vi.fn(),
|
||||||
|
updateWebSearchEmulationConfig: vi.fn(),
|
||||||
|
getAdminApiKey: vi.fn(),
|
||||||
|
getOverloadCooldownSettings: vi.fn(),
|
||||||
|
getStreamTimeoutSettings: vi.fn(),
|
||||||
|
getRectifierSettings: vi.fn(),
|
||||||
|
getBetaPolicySettings: vi.fn(),
|
||||||
|
getGroups: vi.fn(),
|
||||||
|
listProxies: vi.fn(),
|
||||||
|
getProviders: vi.fn(),
|
||||||
|
fetchPublicSettings: vi.fn(),
|
||||||
|
adminSettingsFetch: vi.fn(),
|
||||||
|
showError: vi.fn(),
|
||||||
|
showSuccess: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/api', () => ({
|
||||||
|
adminAPI: {
|
||||||
|
settings: {
|
||||||
|
getSettings,
|
||||||
|
updateSettings,
|
||||||
|
getWebSearchEmulationConfig,
|
||||||
|
updateWebSearchEmulationConfig,
|
||||||
|
getAdminApiKey,
|
||||||
|
getOverloadCooldownSettings,
|
||||||
|
getStreamTimeoutSettings,
|
||||||
|
getRectifierSettings,
|
||||||
|
getBetaPolicySettings,
|
||||||
|
},
|
||||||
|
groups: {
|
||||||
|
getAll: getGroups,
|
||||||
|
},
|
||||||
|
proxies: {
|
||||||
|
list: listProxies,
|
||||||
|
},
|
||||||
|
payment: {
|
||||||
|
getProviders,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/stores', () => ({
|
||||||
|
useAppStore: () => ({
|
||||||
|
showError,
|
||||||
|
showSuccess,
|
||||||
|
showWarning: vi.fn(),
|
||||||
|
showInfo: vi.fn(),
|
||||||
|
fetchPublicSettings,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/stores/adminSettings', () => ({
|
||||||
|
useAdminSettingsStore: () => ({
|
||||||
|
fetch: adminSettingsFetch,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/composables/useClipboard', () => ({
|
||||||
|
useClipboard: () => ({
|
||||||
|
copyToClipboard: vi.fn(),
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/utils/apiError', () => ({
|
||||||
|
extractApiErrorMessage: () => 'error',
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('vue-i18n', async () => {
|
||||||
|
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useI18n: () => ({
|
||||||
|
t: (key: string) => key,
|
||||||
|
locale: ref('zh-CN'),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const AppLayoutStub = { template: '<div><slot /></div>' }
|
||||||
|
const ToggleStub = defineComponent({
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
return () =>
|
||||||
|
h('input', {
|
||||||
|
class: 'toggle-stub',
|
||||||
|
type: 'checkbox',
|
||||||
|
checked: props.modelValue,
|
||||||
|
onChange: (event: Event) => {
|
||||||
|
emit('update:modelValue', (event.target as HTMLInputElement).checked)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const SelectStub = defineComponent({
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: [String, Number, Boolean, null],
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue', 'change'],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const onChange = (event: Event) => {
|
||||||
|
const target = event.target as HTMLSelectElement
|
||||||
|
emit('update:modelValue', target.value)
|
||||||
|
const option = (props.options as Array<Record<string, unknown>>).find(
|
||||||
|
(item) => String(item.value ?? '') === target.value
|
||||||
|
) ?? null
|
||||||
|
emit('change', target.value, option)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () =>
|
||||||
|
h(
|
||||||
|
'select',
|
||||||
|
{
|
||||||
|
class: 'select-stub',
|
||||||
|
value: props.modelValue ?? '',
|
||||||
|
'data-placeholder': props.placeholder,
|
||||||
|
onChange,
|
||||||
|
},
|
||||||
|
(props.options as Array<Record<string, unknown>>).map((option) =>
|
||||||
|
h(
|
||||||
|
'option',
|
||||||
|
{
|
||||||
|
key: `${String(option.value ?? '')}:${String(option.label ?? '')}`,
|
||||||
|
value: option.value as string,
|
||||||
|
},
|
||||||
|
String(option.label ?? '')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const baseSettingsResponse = {
|
||||||
|
registration_enabled: true,
|
||||||
|
email_verify_enabled: false,
|
||||||
|
registration_email_suffix_whitelist: [],
|
||||||
|
promo_code_enabled: true,
|
||||||
|
invitation_code_enabled: false,
|
||||||
|
password_reset_enabled: false,
|
||||||
|
totp_enabled: false,
|
||||||
|
totp_encryption_key_configured: false,
|
||||||
|
default_balance: 0,
|
||||||
|
default_concurrency: 1,
|
||||||
|
default_subscriptions: [],
|
||||||
|
site_name: 'Sub2API',
|
||||||
|
site_logo: '',
|
||||||
|
site_subtitle: '',
|
||||||
|
api_base_url: '',
|
||||||
|
contact_info: '',
|
||||||
|
doc_url: '',
|
||||||
|
home_content: '',
|
||||||
|
hide_ccs_import_button: false,
|
||||||
|
table_default_page_size: 20,
|
||||||
|
table_page_size_options: [10, 20, 50, 100],
|
||||||
|
backend_mode_enabled: false,
|
||||||
|
custom_menu_items: [],
|
||||||
|
custom_endpoints: [],
|
||||||
|
frontend_url: '',
|
||||||
|
smtp_host: '',
|
||||||
|
smtp_port: 587,
|
||||||
|
smtp_username: '',
|
||||||
|
smtp_password_configured: false,
|
||||||
|
smtp_from_email: '',
|
||||||
|
smtp_from_name: '',
|
||||||
|
smtp_use_tls: true,
|
||||||
|
turnstile_enabled: false,
|
||||||
|
turnstile_site_key: '',
|
||||||
|
turnstile_secret_key_configured: false,
|
||||||
|
linuxdo_connect_enabled: false,
|
||||||
|
linuxdo_connect_client_id: '',
|
||||||
|
linuxdo_connect_client_secret_configured: false,
|
||||||
|
linuxdo_connect_redirect_url: '',
|
||||||
|
oidc_connect_enabled: false,
|
||||||
|
oidc_connect_provider_name: 'OIDC',
|
||||||
|
oidc_connect_client_id: '',
|
||||||
|
oidc_connect_client_secret_configured: false,
|
||||||
|
oidc_connect_issuer_url: '',
|
||||||
|
oidc_connect_discovery_url: '',
|
||||||
|
oidc_connect_authorize_url: '',
|
||||||
|
oidc_connect_token_url: '',
|
||||||
|
oidc_connect_userinfo_url: '',
|
||||||
|
oidc_connect_jwks_url: '',
|
||||||
|
oidc_connect_scopes: 'openid email profile',
|
||||||
|
oidc_connect_redirect_url: '',
|
||||||
|
oidc_connect_frontend_redirect_url: '/auth/oidc/callback',
|
||||||
|
oidc_connect_token_auth_method: 'client_secret_post',
|
||||||
|
oidc_connect_use_pkce: true,
|
||||||
|
oidc_connect_validate_id_token: true,
|
||||||
|
oidc_connect_allowed_signing_algs: 'RS256,ES256,PS256',
|
||||||
|
oidc_connect_clock_skew_seconds: 120,
|
||||||
|
oidc_connect_require_email_verified: false,
|
||||||
|
oidc_connect_userinfo_email_path: '',
|
||||||
|
oidc_connect_userinfo_id_path: '',
|
||||||
|
oidc_connect_userinfo_username_path: '',
|
||||||
|
enable_model_fallback: false,
|
||||||
|
fallback_model_anthropic: '',
|
||||||
|
fallback_model_openai: '',
|
||||||
|
fallback_model_gemini: '',
|
||||||
|
fallback_model_antigravity: '',
|
||||||
|
enable_identity_patch: false,
|
||||||
|
identity_patch_prompt: '',
|
||||||
|
ops_monitoring_enabled: false,
|
||||||
|
ops_realtime_monitoring_enabled: false,
|
||||||
|
ops_query_mode_default: 'auto',
|
||||||
|
ops_metrics_interval_seconds: 60,
|
||||||
|
min_claude_code_version: '',
|
||||||
|
max_claude_code_version: '',
|
||||||
|
allow_ungrouped_key_scheduling: false,
|
||||||
|
enable_fingerprint_unification: true,
|
||||||
|
enable_metadata_passthrough: false,
|
||||||
|
enable_cch_signing: false,
|
||||||
|
payment_enabled: true,
|
||||||
|
payment_min_amount: 1,
|
||||||
|
payment_max_amount: 10000,
|
||||||
|
payment_daily_limit: 50000,
|
||||||
|
payment_order_timeout_minutes: 30,
|
||||||
|
payment_max_pending_orders: 3,
|
||||||
|
payment_enabled_types: [],
|
||||||
|
payment_balance_disabled: false,
|
||||||
|
payment_balance_recharge_multiplier: 1,
|
||||||
|
payment_recharge_fee_rate: 0,
|
||||||
|
payment_load_balance_strategy: 'round-robin',
|
||||||
|
payment_product_name_prefix: '',
|
||||||
|
payment_product_name_suffix: '',
|
||||||
|
payment_help_image_url: '',
|
||||||
|
payment_help_text: '',
|
||||||
|
payment_cancel_rate_limit_enabled: false,
|
||||||
|
payment_cancel_rate_limit_max: 10,
|
||||||
|
payment_cancel_rate_limit_window: 1,
|
||||||
|
payment_cancel_rate_limit_unit: 'day',
|
||||||
|
payment_cancel_rate_limit_window_mode: 'rolling',
|
||||||
|
payment_visible_method_alipay_source: 'alipay_direct',
|
||||||
|
payment_visible_method_wxpay_source: 'invalid-source',
|
||||||
|
payment_visible_method_alipay_enabled: true,
|
||||||
|
payment_visible_method_wxpay_enabled: true,
|
||||||
|
openai_advanced_scheduler_enabled: false,
|
||||||
|
balance_low_notify_enabled: false,
|
||||||
|
balance_low_notify_threshold: 0,
|
||||||
|
balance_low_notify_recharge_url: '',
|
||||||
|
account_quota_notify_enabled: false,
|
||||||
|
account_quota_notify_emails: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
function mountView() {
|
||||||
|
return mount(SettingsView, {
|
||||||
|
global: {
|
||||||
|
stubs: {
|
||||||
|
AppLayout: AppLayoutStub,
|
||||||
|
Select: SelectStub,
|
||||||
|
Toggle: ToggleStub,
|
||||||
|
Icon: true,
|
||||||
|
ConfirmDialog: true,
|
||||||
|
PaymentProviderList: true,
|
||||||
|
PaymentProviderDialog: true,
|
||||||
|
GroupBadge: true,
|
||||||
|
GroupOptionItem: true,
|
||||||
|
ProxySelector: true,
|
||||||
|
ImageUpload: true,
|
||||||
|
BackupSettings: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openPaymentTab(wrapper: ReturnType<typeof mountView>) {
|
||||||
|
const paymentTabButton = wrapper
|
||||||
|
.findAll('button')
|
||||||
|
.find((node) => node.text().includes('admin.settings.tabs.payment'))
|
||||||
|
|
||||||
|
expect(paymentTabButton).toBeDefined()
|
||||||
|
await paymentTabButton?.trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('admin SettingsView payment visible method controls', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
getSettings.mockReset()
|
||||||
|
updateSettings.mockReset()
|
||||||
|
getWebSearchEmulationConfig.mockReset()
|
||||||
|
updateWebSearchEmulationConfig.mockReset()
|
||||||
|
getAdminApiKey.mockReset()
|
||||||
|
getOverloadCooldownSettings.mockReset()
|
||||||
|
getStreamTimeoutSettings.mockReset()
|
||||||
|
getRectifierSettings.mockReset()
|
||||||
|
getBetaPolicySettings.mockReset()
|
||||||
|
getGroups.mockReset()
|
||||||
|
listProxies.mockReset()
|
||||||
|
getProviders.mockReset()
|
||||||
|
fetchPublicSettings.mockReset()
|
||||||
|
adminSettingsFetch.mockReset()
|
||||||
|
showError.mockReset()
|
||||||
|
showSuccess.mockReset()
|
||||||
|
|
||||||
|
getSettings.mockResolvedValue({ ...baseSettingsResponse })
|
||||||
|
updateSettings.mockImplementation(async (payload) => ({
|
||||||
|
...baseSettingsResponse,
|
||||||
|
...payload,
|
||||||
|
}))
|
||||||
|
getWebSearchEmulationConfig.mockResolvedValue({
|
||||||
|
enabled: false,
|
||||||
|
providers: [],
|
||||||
|
})
|
||||||
|
updateWebSearchEmulationConfig.mockResolvedValue({
|
||||||
|
enabled: false,
|
||||||
|
providers: [],
|
||||||
|
})
|
||||||
|
getAdminApiKey.mockResolvedValue({
|
||||||
|
exists: false,
|
||||||
|
masked_key: '',
|
||||||
|
})
|
||||||
|
getOverloadCooldownSettings.mockResolvedValue({
|
||||||
|
enabled: true,
|
||||||
|
cooldown_minutes: 10,
|
||||||
|
})
|
||||||
|
getStreamTimeoutSettings.mockResolvedValue({
|
||||||
|
enabled: true,
|
||||||
|
action: 'temp_unsched',
|
||||||
|
temp_unsched_minutes: 5,
|
||||||
|
threshold_count: 3,
|
||||||
|
threshold_window_minutes: 10,
|
||||||
|
})
|
||||||
|
getRectifierSettings.mockResolvedValue({
|
||||||
|
enabled: true,
|
||||||
|
thinking_signature_enabled: true,
|
||||||
|
thinking_budget_enabled: true,
|
||||||
|
apikey_signature_enabled: false,
|
||||||
|
apikey_signature_patterns: [],
|
||||||
|
})
|
||||||
|
getBetaPolicySettings.mockResolvedValue({
|
||||||
|
rules: [],
|
||||||
|
})
|
||||||
|
getGroups.mockResolvedValue([])
|
||||||
|
listProxies.mockResolvedValue({
|
||||||
|
items: [],
|
||||||
|
})
|
||||||
|
getProviders.mockResolvedValue({
|
||||||
|
data: [],
|
||||||
|
})
|
||||||
|
fetchPublicSettings.mockResolvedValue(undefined)
|
||||||
|
adminSettingsFetch.mockResolvedValue(undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('loads canonical source options and normalizes existing values', async () => {
|
||||||
|
const wrapper = mountView()
|
||||||
|
|
||||||
|
await flushPromises()
|
||||||
|
await openPaymentTab(wrapper)
|
||||||
|
|
||||||
|
const paymentSourceSelects = wrapper
|
||||||
|
.findAll('select.select-stub')
|
||||||
|
.filter((node) => ['alipay', 'wxpay'].includes(node.attributes('data-placeholder')))
|
||||||
|
|
||||||
|
expect(paymentSourceSelects).toHaveLength(2)
|
||||||
|
|
||||||
|
const alipaySelect = paymentSourceSelects.find(
|
||||||
|
(node) => node.attributes('data-placeholder') === 'alipay'
|
||||||
|
)
|
||||||
|
const wxpaySelect = paymentSourceSelects.find(
|
||||||
|
(node) => node.attributes('data-placeholder') === 'wxpay'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(alipaySelect?.element.value).toBe('official_alipay')
|
||||||
|
expect(alipaySelect?.findAll('option').map((option) => option.element.value)).toEqual([
|
||||||
|
'',
|
||||||
|
'official_alipay',
|
||||||
|
'easypay_alipay',
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(wxpaySelect?.element.value).toBe('')
|
||||||
|
expect(wxpaySelect?.findAll('option').map((option) => option.element.value)).toEqual([
|
||||||
|
'',
|
||||||
|
'official_wxpay',
|
||||||
|
'easypay_wxpay',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('saves canonical source keys selected from the dropdowns', async () => {
|
||||||
|
const wrapper = mountView()
|
||||||
|
|
||||||
|
await flushPromises()
|
||||||
|
await openPaymentTab(wrapper)
|
||||||
|
|
||||||
|
const paymentSourceSelects = wrapper
|
||||||
|
.findAll('select.select-stub')
|
||||||
|
.filter((node) => ['alipay', 'wxpay'].includes(node.attributes('data-placeholder')))
|
||||||
|
|
||||||
|
const alipaySelect = paymentSourceSelects.find(
|
||||||
|
(node) => node.attributes('data-placeholder') === 'alipay'
|
||||||
|
)
|
||||||
|
const wxpaySelect = paymentSourceSelects.find(
|
||||||
|
(node) => node.attributes('data-placeholder') === 'wxpay'
|
||||||
|
)
|
||||||
|
|
||||||
|
await alipaySelect?.setValue('easypay_alipay')
|
||||||
|
await wxpaySelect?.setValue('official_wxpay')
|
||||||
|
await wrapper.find('form').trigger('submit.prevent')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(updateSettings).toHaveBeenCalledTimes(1)
|
||||||
|
expect(updateSettings).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
payment_visible_method_alipay_source: 'easypay_alipay',
|
||||||
|
payment_visible_method_wxpay_source: 'official_wxpay',
|
||||||
|
payment_visible_method_alipay_enabled: true,
|
||||||
|
payment_visible_method_wxpay_enabled: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user