fix(settings): restore wechat and payment config persistence
This commit is contained in:
21
frontend/src/api/__tests__/settings.wechatConnect.spec.ts
Normal file
21
frontend/src/api/__tests__/settings.wechatConnect.spec.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
defaultWeChatConnectScopesForMode,
|
||||
normalizeWeChatConnectMode,
|
||||
} from "@/api/admin/settings";
|
||||
|
||||
describe("admin settings wechat connect helpers", () => {
|
||||
it("normalizes legacy or noisy mode values to the backend contract", () => {
|
||||
expect(normalizeWeChatConnectMode("OPEN")).toBe("open");
|
||||
expect(normalizeWeChatConnectMode(" open_platform ")).toBe("open");
|
||||
expect(normalizeWeChatConnectMode("mp")).toBe("mp");
|
||||
expect(normalizeWeChatConnectMode("official_account")).toBe("mp");
|
||||
expect(normalizeWeChatConnectMode("unknown")).toBe("open");
|
||||
});
|
||||
|
||||
it("maps each mode to the backend default scopes", () => {
|
||||
expect(defaultWeChatConnectScopesForMode("open")).toBe("snsapi_login");
|
||||
expect(defaultWeChatConnectScopesForMode("mp")).toBe("snsapi_userinfo");
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { defineComponent, h, ref } from 'vue'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
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'
|
||||
import SettingsView from "../SettingsView.vue";
|
||||
|
||||
const {
|
||||
getSettings,
|
||||
@@ -38,9 +38,9 @@ const {
|
||||
adminSettingsFetch: vi.fn(),
|
||||
showError: vi.fn(),
|
||||
showSuccess: vi.fn(),
|
||||
}))
|
||||
}));
|
||||
|
||||
vi.mock('@/api', () => ({
|
||||
vi.mock("@/api", () => ({
|
||||
adminAPI: {
|
||||
settings: {
|
||||
getSettings,
|
||||
@@ -63,9 +63,9 @@ vi.mock('@/api', () => ({
|
||||
getProviders,
|
||||
},
|
||||
},
|
||||
}))
|
||||
}));
|
||||
|
||||
vi.mock('@/stores', () => ({
|
||||
vi.mock("@/stores", () => ({
|
||||
useAppStore: () => ({
|
||||
showError,
|
||||
showSuccess,
|
||||
@@ -73,36 +73,36 @@ vi.mock('@/stores', () => ({
|
||||
showInfo: vi.fn(),
|
||||
fetchPublicSettings,
|
||||
}),
|
||||
}))
|
||||
}));
|
||||
|
||||
vi.mock('@/stores/adminSettings', () => ({
|
||||
vi.mock("@/stores/adminSettings", () => ({
|
||||
useAdminSettingsStore: () => ({
|
||||
fetch: adminSettingsFetch,
|
||||
}),
|
||||
}))
|
||||
}));
|
||||
|
||||
vi.mock('@/composables/useClipboard', () => ({
|
||||
vi.mock("@/composables/useClipboard", () => ({
|
||||
useClipboard: () => ({
|
||||
copyToClipboard: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/apiError', () => ({
|
||||
extractApiErrorMessage: () => 'error',
|
||||
}))
|
||||
vi.mock("@/utils/apiError", () => ({
|
||||
extractApiErrorMessage: () => "error",
|
||||
}));
|
||||
|
||||
vi.mock('vue-i18n', async () => {
|
||||
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
|
||||
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'),
|
||||
locale: ref("zh-CN"),
|
||||
}),
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
const AppLayoutStub = { template: '<div><slot /></div>' }
|
||||
const AppLayoutStub = { template: "<div><slot /></div>" };
|
||||
const ToggleStub = defineComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
@@ -110,25 +110,25 @@ const ToggleStub = defineComponent({
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, { emit }) {
|
||||
return () =>
|
||||
h('input', {
|
||||
class: 'toggle-stub',
|
||||
type: 'checkbox',
|
||||
h("input", {
|
||||
class: "toggle-stub",
|
||||
type: "checkbox",
|
||||
checked: props.modelValue,
|
||||
onChange: (event: Event) => {
|
||||
emit('update:modelValue', (event.target as HTMLInputElement).checked)
|
||||
emit("update:modelValue", (event.target as HTMLInputElement).checked);
|
||||
},
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const SelectStub = defineComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [String, Number, Boolean, null],
|
||||
default: '',
|
||||
default: "",
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
@@ -136,42 +136,43 @@ const SelectStub = defineComponent({
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue', 'change'],
|
||||
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)
|
||||
}
|
||||
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',
|
||||
"select",
|
||||
{
|
||||
class: 'select-stub',
|
||||
value: props.modelValue ?? '',
|
||||
'data-placeholder': props.placeholder,
|
||||
class: "select-stub",
|
||||
value: props.modelValue ?? "",
|
||||
"data-placeholder": props.placeholder,
|
||||
onChange,
|
||||
},
|
||||
(props.options as Array<Record<string, unknown>>).map((option) =>
|
||||
h(
|
||||
'option',
|
||||
"option",
|
||||
{
|
||||
key: `${String(option.value ?? '')}:${String(option.label ?? '')}`,
|
||||
key: `${String(option.value ?? "")}:${String(option.label ?? "")}`,
|
||||
value: option.value as string,
|
||||
},
|
||||
String(option.label ?? '')
|
||||
)
|
||||
)
|
||||
)
|
||||
String(option.label ?? ""),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const baseSettingsResponse = {
|
||||
registration_enabled: true,
|
||||
@@ -185,69 +186,77 @@ const baseSettingsResponse = {
|
||||
default_balance: 0,
|
||||
default_concurrency: 1,
|
||||
default_subscriptions: [],
|
||||
site_name: 'Sub2API',
|
||||
site_logo: '',
|
||||
site_subtitle: '',
|
||||
api_base_url: '',
|
||||
contact_info: '',
|
||||
doc_url: '',
|
||||
home_content: '',
|
||||
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: '',
|
||||
frontend_url: "",
|
||||
smtp_host: "",
|
||||
smtp_port: 587,
|
||||
smtp_username: '',
|
||||
smtp_username: "",
|
||||
smtp_password_configured: false,
|
||||
smtp_from_email: '',
|
||||
smtp_from_name: '',
|
||||
smtp_from_email: "",
|
||||
smtp_from_name: "",
|
||||
smtp_use_tls: true,
|
||||
turnstile_enabled: false,
|
||||
turnstile_site_key: '',
|
||||
turnstile_site_key: "",
|
||||
turnstile_secret_key_configured: false,
|
||||
linuxdo_connect_enabled: false,
|
||||
linuxdo_connect_client_id: '',
|
||||
linuxdo_connect_client_id: "",
|
||||
linuxdo_connect_client_secret_configured: false,
|
||||
linuxdo_connect_redirect_url: '',
|
||||
linuxdo_connect_redirect_url: "",
|
||||
wechat_connect_enabled: true,
|
||||
wechat_connect_app_id: "wx-app-id-123",
|
||||
wechat_connect_app_secret_configured: true,
|
||||
wechat_connect_mode: "mp",
|
||||
wechat_connect_scopes: "",
|
||||
wechat_connect_redirect_url:
|
||||
"https://admin.example.com/api/v1/auth/oauth/wechat/callback",
|
||||
wechat_connect_frontend_redirect_url: "/auth/wechat/callback",
|
||||
oidc_connect_enabled: false,
|
||||
oidc_connect_provider_name: 'OIDC',
|
||||
oidc_connect_client_id: '',
|
||||
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_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_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: '',
|
||||
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: '',
|
||||
fallback_model_anthropic: "",
|
||||
fallback_model_openai: "",
|
||||
fallback_model_gemini: "",
|
||||
fallback_model_antigravity: "",
|
||||
enable_identity_patch: false,
|
||||
identity_patch_prompt: '',
|
||||
identity_patch_prompt: "",
|
||||
ops_monitoring_enabled: false,
|
||||
ops_realtime_monitoring_enabled: false,
|
||||
ops_query_mode_default: 'auto',
|
||||
ops_query_mode_default: "auto",
|
||||
ops_metrics_interval_seconds: 60,
|
||||
min_claude_code_version: '',
|
||||
max_claude_code_version: '',
|
||||
min_claude_code_version: "",
|
||||
max_claude_code_version: "",
|
||||
allow_ungrouped_key_scheduling: false,
|
||||
enable_fingerprint_unification: true,
|
||||
enable_metadata_passthrough: false,
|
||||
@@ -262,27 +271,27 @@ const baseSettingsResponse = {
|
||||
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_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_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: '',
|
||||
balance_low_notify_recharge_url: "",
|
||||
account_quota_notify_enabled: false,
|
||||
account_quota_notify_emails: [],
|
||||
}
|
||||
};
|
||||
|
||||
function mountView() {
|
||||
return mount(SettingsView, {
|
||||
@@ -302,184 +311,361 @@ function mountView() {
|
||||
BackupSettings: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async function openPaymentTab(wrapper: ReturnType<typeof mountView>) {
|
||||
const paymentTabButton = wrapper
|
||||
.findAll('button')
|
||||
.find((node) => node.text().includes('admin.settings.tabs.payment'))
|
||||
.findAll("button")
|
||||
.find((node) => node.text().includes("admin.settings.tabs.payment"));
|
||||
|
||||
expect(paymentTabButton).toBeDefined()
|
||||
await paymentTabButton?.trigger('click')
|
||||
await flushPromises()
|
||||
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()
|
||||
async function openSecurityTab(wrapper: ReturnType<typeof mountView>) {
|
||||
const securityTabButton = wrapper
|
||||
.findAll("button")
|
||||
.find((node) => node.text().includes("admin.settings.tabs.security"));
|
||||
|
||||
getSettings.mockResolvedValue({ ...baseSettingsResponse })
|
||||
expect(securityTabButton).toBeDefined();
|
||||
await securityTabButton?.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: '',
|
||||
})
|
||||
masked_key: "",
|
||||
});
|
||||
getOverloadCooldownSettings.mockResolvedValue({
|
||||
enabled: true,
|
||||
cooldown_minutes: 10,
|
||||
})
|
||||
});
|
||||
getStreamTimeoutSettings.mockResolvedValue({
|
||||
enabled: true,
|
||||
action: 'temp_unsched',
|
||||
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([])
|
||||
});
|
||||
getGroups.mockResolvedValue([]);
|
||||
listProxies.mockResolvedValue({
|
||||
items: [],
|
||||
})
|
||||
});
|
||||
getProviders.mockResolvedValue({
|
||||
data: [],
|
||||
})
|
||||
fetchPublicSettings.mockResolvedValue(undefined)
|
||||
adminSettingsFetch.mockResolvedValue(undefined)
|
||||
})
|
||||
});
|
||||
fetchPublicSettings.mockResolvedValue(undefined);
|
||||
adminSettingsFetch.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('loads canonical source options and normalizes existing values', async () => {
|
||||
const wrapper = mountView()
|
||||
it("loads canonical source options and normalizes existing values", async () => {
|
||||
const wrapper = mountView();
|
||||
|
||||
await flushPromises()
|
||||
await openPaymentTab(wrapper)
|
||||
await flushPromises();
|
||||
await openPaymentTab(wrapper);
|
||||
|
||||
const paymentSourceSelects = wrapper
|
||||
.findAll('select.select-stub')
|
||||
.filter((node) => ['alipay', 'wxpay'].includes(node.attributes('data-placeholder')))
|
||||
.findAll("select.select-stub")
|
||||
.filter((node) =>
|
||||
["alipay", "wxpay"].includes(node.attributes("data-placeholder")),
|
||||
);
|
||||
|
||||
expect(paymentSourceSelects).toHaveLength(2)
|
||||
expect(paymentSourceSelects).toHaveLength(2);
|
||||
|
||||
const alipaySelect = paymentSourceSelects.find(
|
||||
(node) => node.attributes('data-placeholder') === 'alipay'
|
||||
)
|
||||
(node) => node.attributes("data-placeholder") === "alipay",
|
||||
);
|
||||
const wxpaySelect = paymentSourceSelects.find(
|
||||
(node) => node.attributes('data-placeholder') === 'wxpay'
|
||||
)
|
||||
(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(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',
|
||||
])
|
||||
})
|
||||
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()
|
||||
it("saves canonical source keys selected from the dropdowns", async () => {
|
||||
const wrapper = mountView();
|
||||
|
||||
await flushPromises()
|
||||
await openPaymentTab(wrapper)
|
||||
await flushPromises();
|
||||
await openPaymentTab(wrapper);
|
||||
|
||||
const paymentSourceSelects = wrapper
|
||||
.findAll('select.select-stub')
|
||||
.filter((node) => ['alipay', 'wxpay'].includes(node.attributes('data-placeholder')))
|
||||
.findAll("select.select-stub")
|
||||
.filter((node) =>
|
||||
["alipay", "wxpay"].includes(node.attributes("data-placeholder")),
|
||||
);
|
||||
|
||||
const alipaySelect = paymentSourceSelects.find(
|
||||
(node) => node.attributes('data-placeholder') === 'alipay'
|
||||
)
|
||||
(node) => node.attributes("data-placeholder") === "alipay",
|
||||
);
|
||||
const wxpaySelect = paymentSourceSelects.find(
|
||||
(node) => node.attributes('data-placeholder') === 'wxpay'
|
||||
)
|
||||
(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()
|
||||
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).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_source: "easypay_alipay",
|
||||
payment_visible_method_wxpay_source: "official_wxpay",
|
||||
payment_visible_method_alipay_enabled: true,
|
||||
payment_visible_method_wxpay_enabled: true,
|
||||
})
|
||||
)
|
||||
})
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('blocks saving when a visible payment method is enabled without a source', async () => {
|
||||
const wrapper = mountView()
|
||||
it("blocks saving when a visible payment method is enabled without a source", async () => {
|
||||
const wrapper = mountView();
|
||||
|
||||
await flushPromises()
|
||||
await openPaymentTab(wrapper)
|
||||
await flushPromises();
|
||||
await openPaymentTab(wrapper);
|
||||
|
||||
const paymentSourceSelects = wrapper
|
||||
.findAll('select.select-stub')
|
||||
.filter((node) => ['alipay', 'wxpay'].includes(node.attributes('data-placeholder')))
|
||||
.findAll("select.select-stub")
|
||||
.filter((node) =>
|
||||
["alipay", "wxpay"].includes(node.attributes("data-placeholder")),
|
||||
);
|
||||
|
||||
const alipaySelect = paymentSourceSelects.find(
|
||||
(node) => node.attributes('data-placeholder') === 'alipay'
|
||||
)
|
||||
(node) => node.attributes("data-placeholder") === "alipay",
|
||||
);
|
||||
|
||||
await alipaySelect?.setValue('')
|
||||
await wrapper.find('form').trigger('submit.prevent')
|
||||
await flushPromises()
|
||||
await alipaySelect?.setValue("");
|
||||
await wrapper.find("form").trigger("submit.prevent");
|
||||
await flushPromises();
|
||||
|
||||
expect(updateSettings).not.toHaveBeenCalled()
|
||||
expect(showError).toHaveBeenCalled()
|
||||
expect(String(showError.mock.calls.at(-1)?.[0] ?? '')).toContain('支付来源')
|
||||
})
|
||||
expect(updateSettings).not.toHaveBeenCalled();
|
||||
expect(showError).toHaveBeenCalled();
|
||||
expect(String(showError.mock.calls.at(-1)?.[0] ?? "")).toContain(
|
||||
"支付来源",
|
||||
);
|
||||
});
|
||||
|
||||
it('renders advanced scheduler copy as local experimental gateway policy', async () => {
|
||||
const wrapper = mountView()
|
||||
it("renders advanced scheduler copy as local experimental gateway policy", async () => {
|
||||
const wrapper = mountView();
|
||||
|
||||
await flushPromises()
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.text()).toContain('OpenAI 实验调度策略')
|
||||
expect(wrapper.text()).toContain('默认关闭。开启后仅影响本网关在 OpenAI 账号间的实验性调度选择逻辑')
|
||||
expect(wrapper.text()).not.toContain('OpenAI 高级调度器')
|
||||
})
|
||||
})
|
||||
expect(wrapper.text()).toContain("OpenAI 实验调度策略");
|
||||
expect(wrapper.text()).toContain(
|
||||
"默认关闭。开启后仅影响本网关在 OpenAI 账号间的实验性调度选择逻辑",
|
||||
);
|
||||
expect(wrapper.text()).not.toContain("OpenAI 高级调度器");
|
||||
});
|
||||
});
|
||||
|
||||
describe("admin SettingsView wechat connect 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,
|
||||
payment_visible_method_wxpay_source: "official_wxpay",
|
||||
});
|
||||
updateSettings.mockImplementation(async (payload) => ({
|
||||
...baseSettingsResponse,
|
||||
payment_visible_method_wxpay_source: "official_wxpay",
|
||||
...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 and echoes WeChat Connect fields from the backend payload", async () => {
|
||||
const wrapper = mountView();
|
||||
|
||||
await flushPromises();
|
||||
await openSecurityTab(wrapper);
|
||||
|
||||
expect(
|
||||
(
|
||||
wrapper.get('[data-testid="wechat-connect-app-id"]')
|
||||
.element as HTMLInputElement
|
||||
).value,
|
||||
).toBe("wx-app-id-123");
|
||||
expect(
|
||||
(
|
||||
wrapper.get('[data-testid="wechat-connect-mode"]')
|
||||
.element as HTMLSelectElement
|
||||
).value,
|
||||
).toBe("mp");
|
||||
expect(
|
||||
(
|
||||
wrapper.get('[data-testid="wechat-connect-scopes"]')
|
||||
.element as HTMLInputElement
|
||||
).value,
|
||||
).toBe("snsapi_userinfo");
|
||||
expect(
|
||||
wrapper
|
||||
.get('[data-testid="wechat-connect-app-secret"]')
|
||||
.attributes("placeholder"),
|
||||
).toContain("密钥已配置");
|
||||
expect(
|
||||
(
|
||||
wrapper.get('[data-testid="wechat-connect-frontend-redirect-url"]')
|
||||
.element as HTMLInputElement
|
||||
).value,
|
||||
).toBe("/auth/wechat/callback");
|
||||
});
|
||||
|
||||
it("saves WeChat Connect fields using the backend contract and clears the secret after save", async () => {
|
||||
const wrapper = mountView();
|
||||
|
||||
await flushPromises();
|
||||
await openSecurityTab(wrapper);
|
||||
|
||||
await wrapper
|
||||
.get('[data-testid="wechat-connect-app-id"]')
|
||||
.setValue("wx-app-id-updated");
|
||||
await wrapper
|
||||
.get('[data-testid="wechat-connect-app-secret"]')
|
||||
.setValue("new-secret");
|
||||
await wrapper.get('[data-testid="wechat-connect-mode"]').setValue("open");
|
||||
await wrapper
|
||||
.get('[data-testid="wechat-connect-scopes"]')
|
||||
.setValue(" snsapi_base ");
|
||||
await wrapper
|
||||
.get('[data-testid="wechat-connect-redirect-url"]')
|
||||
.setValue("https://admin.example.com/api/v1/auth/oauth/wechat/callback");
|
||||
await wrapper
|
||||
.get('[data-testid="wechat-connect-frontend-redirect-url"]')
|
||||
.setValue("/auth/wechat/callback");
|
||||
await wrapper.find("form").trigger("submit.prevent");
|
||||
await flushPromises();
|
||||
|
||||
expect(updateSettings).toHaveBeenCalledTimes(1);
|
||||
expect(updateSettings).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
wechat_connect_enabled: true,
|
||||
wechat_connect_app_id: "wx-app-id-updated",
|
||||
wechat_connect_app_secret: "new-secret",
|
||||
wechat_connect_mode: "open",
|
||||
wechat_connect_scopes: "snsapi_base",
|
||||
wechat_connect_redirect_url:
|
||||
"https://admin.example.com/api/v1/auth/oauth/wechat/callback",
|
||||
wechat_connect_frontend_redirect_url: "/auth/wechat/callback",
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
(
|
||||
wrapper.get('[data-testid="wechat-connect-app-secret"]')
|
||||
.element as HTMLInputElement
|
||||
).value,
|
||||
).toBe("");
|
||||
expect(
|
||||
wrapper
|
||||
.get('[data-testid="wechat-connect-app-secret"]')
|
||||
.attributes("placeholder"),
|
||||
).toContain("密钥已配置");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user