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("vue-i18n"); const translations: Record = { "admin.settings.wechatConnect.title": "微信登录", "admin.settings.wechatConnect.description": "用于微信开放平台或公众号/小程序的第三方登录配置。", "admin.settings.wechatConnect.enabledLabel": "启用微信登录", "admin.settings.wechatConnect.enabledHint": "开启后可使用微信第三方登录回调与授权配置。", "admin.settings.wechatConnect.appIdLabel": "AppID", "admin.settings.wechatConnect.appIdPlaceholder": "微信开放平台 AppID", "admin.settings.wechatConnect.appSecretLabel": "AppSecret", "admin.settings.wechatConnect.appSecretConfiguredPlaceholder": "密钥已配置,留空以保留当前值。", "admin.settings.wechatConnect.appSecretPlaceholder": "微信开放平台 AppSecret", "admin.settings.wechatConnect.appSecretConfiguredHint": "密钥已配置,留空以保留当前值。", "admin.settings.wechatConnect.appSecretHint": "填写后会覆盖当前微信密钥。", "admin.settings.wechatConnect.modeLabel": "模式", "admin.settings.wechatConnect.openModeLabel": "非微信环境使用开放平台", "admin.settings.wechatConnect.openModeHint": "浏览器不在微信内时,自动走开放平台扫码授权。", "admin.settings.wechatConnect.mpModeLabel": "微信环境使用公众号", "admin.settings.wechatConnect.mpModeHint": "浏览器在微信内时,自动走公众号授权。", "admin.settings.wechatConnect.redirectUrlLabel": "回调地址", "admin.settings.wechatConnect.redirectUrlPlaceholder": "https://your-site.com/api/v1/auth/oauth/wechat/callback", "admin.settings.wechatConnect.generateAndCopy": "使用当前站点生成并复制", "admin.settings.wechatConnect.redirectUrlSetAndCopied": "已使用当前站点生成回调地址并复制到剪贴板", "admin.settings.wechatConnect.frontendRedirectUrlLabel": "前端回调地址", "admin.settings.wechatConnect.frontendRedirectUrlPlaceholder": "/auth/wechat/callback", "admin.settings.wechatConnect.frontendRedirectUrlHint": "通常用于前端路由回调地址,需与后端配置保持一致。", "admin.settings.authSourceDefaults.title": "认证来源默认值", "admin.settings.authSourceDefaults.description": "按注册来源配置新用户默认余额、并发、订阅与授权策略。", "admin.settings.authSourceDefaults.requireEmailLabel": "第三方注册强制补充邮箱", "admin.settings.authSourceDefaults.requireEmailHint": "启用后,Linux DO、OIDC、微信注册缺少邮箱时必须先补充邮箱地址。", "admin.settings.authSourceDefaults.enabledHint": "以下默认值会在该来源注册新用户时发放;首次绑定时授权仅作用于已有账号绑定该来源。", "admin.settings.authSourceDefaults.sources.email.title": "邮箱注册", "admin.settings.authSourceDefaults.sources.email.description": "适用于邮箱密码注册的新用户默认配额。", "admin.settings.authSourceDefaults.sources.linuxdo.title": "Linux DO 登录", "admin.settings.authSourceDefaults.sources.linuxdo.description": "适用于 Linux DO 第三方注册的新用户默认配额。", "admin.settings.authSourceDefaults.sources.oidc.title": "OIDC 登录", "admin.settings.authSourceDefaults.sources.oidc.description": "适用于 OIDC 第三方注册的新用户默认配额。", "admin.settings.authSourceDefaults.sources.wechat.title": "微信登录", "admin.settings.authSourceDefaults.sources.wechat.description": "适用于微信第三方注册的新用户默认配额。", "admin.settings.authSourceDefaults.grantOnFirstBindLabel": "首次绑定时授权", "admin.settings.authSourceDefaults.grantOnFirstBindHint": "已有账号首次绑定该来源时发放默认权益。", "admin.settings.authSourceDefaults.defaultSubscriptionsLabel": "默认订阅", "admin.settings.authSourceDefaults.defaultSubscriptionsHint": "仅对当前认证来源生效,未配置时不追加来源专属订阅。", "admin.settings.authSourceDefaults.noSourceSubscriptions": "当前来源未配置专属默认订阅。", "admin.settings.paymentVisibleMethods.methodLabel": "{title} 可见方式", "admin.settings.paymentVisibleMethods.methodHint": "控制前台结算页是否展示该方式,以及展示时使用的来源键。", "admin.settings.paymentVisibleMethods.sourceLabel": "支付来源", "admin.settings.paymentVisibleMethods.sourceHint": "启用后必须明确选择一个来源;未配置状态不会对外展示该支付方式。", "admin.settings.paymentVisibleMethods.sourceRequiredError": "{title} 已启用,请先选择支付来源。", "admin.settings.openaiExperimentalScheduler.title": "OpenAI 实验调度策略", "admin.settings.openaiExperimentalScheduler.description": "默认关闭。开启后仅影响本网关在 OpenAI 账号间的实验性调度选择逻辑,不代表上游 OpenAI 官方能力。", }; return { ...actual, useI18n: () => ({ t: (key: string, params?: Record) => (translations[key] ?? key).replace(/\{(\w+)\}/g, (_, token) => params?.[token] ?? `{${token}}`), locale: ref("zh-CN"), }), }; }); const AppLayoutStub = { template: "
" }; const ToggleStub = defineComponent({ props: { modelValue: { type: Boolean, default: false, }, }, emits: ["update:modelValue"], inheritAttrs: false, setup(props, { attrs, emit }) { return () => h("input", { ...attrs, 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>).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>).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: "", wechat_connect_enabled: true, wechat_connect_app_id: "wx-app-id-123", wechat_connect_app_secret_configured: true, wechat_connect_open_enabled: false, wechat_connect_mp_enabled: 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_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) { const paymentTabButton = wrapper .findAll("button") .find((node) => node.text().includes("admin.settings.tabs.payment")); expect(paymentTabButton).toBeDefined(); await paymentTabButton?.trigger("click"); await flushPromises(); } async function openSecurityTab(wrapper: ReturnType) { const securityTabButton = wrapper .findAll("button") .find((node) => node.text().includes("admin.settings.tabs.security")); expect(securityTabButton).toBeDefined(); await securityTabButton?.trigger("click"); await flushPromises(); } async function openUsersTab(wrapper: ReturnType) { const usersTabButton = wrapper .findAll("button") .find((node) => node.text().includes("admin.settings.tabs.users")); expect(usersTabButton).toBeDefined(); await usersTabButton?.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, }), ); }); it("blocks saving when a visible payment method is enabled without a source", 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", ); 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( "支付来源", ); }); it("renders advanced scheduler copy as local experimental gateway policy", async () => { const wrapper = mountView(); await flushPromises(); 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-open-enabled"]') .element as HTMLInputElement ).checked, ).toBe(false); expect( ( wrapper.get('[data-testid="wechat-connect-mp-enabled"]') .element as HTMLInputElement ).checked, ).toBe(true); expect(wrapper.find('[data-testid="wechat-connect-scopes"]').exists()).toBe( false, ); 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-open-enabled"]') .setValue(true); await wrapper .get('[data-testid="wechat-connect-mp-enabled"]') .setValue(true); 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_open_enabled: true, wechat_connect_mp_enabled: true, 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("密钥已配置"); }); it("collapses auth source defaults until the source is enabled", async () => { const wrapper = mountView(); await flushPromises(); await openUsersTab(wrapper); expect( ( wrapper.get('[data-testid="auth-source-email-enabled"]') .element as HTMLInputElement ).checked, ).toBe(false); expect( wrapper.find('[data-testid="auth-source-email-panel"]').exists(), ).toBe(false); expect(wrapper.text()).not.toContain("注册即授权"); await wrapper .get('[data-testid="auth-source-email-enabled"]') .setValue(true); expect( wrapper.find('[data-testid="auth-source-email-panel"]').exists(), ).toBe(true); expect(wrapper.text()).toContain("首次绑定时授权"); }); });