import { beforeEach, describe, expect, it, vi } from "vitest"; import { defineComponent, h } 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, updateProvider, createProvider, deleteProvider, 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(), updateProvider: vi.fn(), createProvider: vi.fn(), deleteProvider: vi.fn(), fetchPublicSettings: vi.fn(), adminSettingsFetch: vi.fn(), showError: vi.fn(), showSuccess: vi.fn(), })); const localeRef = vi.hoisted(() => ({ value: "zh-CN" })); vi.mock("@/api", () => ({ adminAPI: { settings: { getSettings, updateSettings, getWebSearchEmulationConfig, updateWebSearchEmulationConfig, getAdminApiKey, getOverloadCooldownSettings, getStreamTimeoutSettings, getRectifierSettings, getBetaPolicySettings, }, groups: { getAll: getGroups, }, proxies: { list: listProxies, }, payment: { getProviders, updateProvider, createProvider, deleteProvider, }, }, })); 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.payment.configGuide": "查看支付配置说明", "admin.settings.payment.findProvider": "查看支持的支付方式", "admin.settings.openaiExperimentalScheduler.title": "OpenAI 实验调度策略", "admin.settings.openaiExperimentalScheduler.description": "默认关闭。开启后仅影响本网关在 OpenAI 账号间的实验性调度选择逻辑,不代表上游 OpenAI 官方能力。", "admin.settings.site.uploadImage": "上传图片", "admin.settings.site.remove": "移除", }; return { ...actual, useI18n: () => ({ t: (key: string, params?: Record) => (translations[key] ?? key).replace(/\{(\w+)\}/g, (_, token) => params?.[token] ?? `{${token}}`), locale: localeRef, }), }; }); 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 ImageUploadStub = defineComponent({ props: { modelValue: { type: String, default: "", }, uploadLabel: { type: String, default: "", }, removeLabel: { type: String, default: "", }, placeholder: { type: String, default: "", }, }, setup(props) { return () => h("div", { class: "image-upload-stub", "data-model-value": props.modelValue, "data-upload-label": props.uploadLabel, "data-remove-label": props.removeLabel, "data-placeholder": props.placeholder, }); }, }); 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: ImageUploadStub, 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(); updateProvider.mockReset(); createProvider.mockReset(); deleteProvider.mockReset(); fetchPublicSettings.mockReset(); adminSettingsFetch.mockReset(); showError.mockReset(); showSuccess.mockReset(); localeRef.value = "zh-CN"; 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("does not render legacy visible payment method controls", async () => { const wrapper = mountView(); await flushPromises(); await openPaymentTab(wrapper); expect(wrapper.text()).not.toContain("可见方式"); expect(wrapper.text()).not.toContain("支付来源"); }); it("links payment guidance to README sections instead of removed payment docs", async () => { const wrapper = mountView(); await flushPromises(); await openPaymentTab(wrapper); const paymentLinks = wrapper .findAll("a") .filter((node) => ["查看支付配置说明", "查看支持的支付方式"].includes(node.text()), ); expect(paymentLinks).toHaveLength(2); expect(paymentLinks[0]?.attributes("href")).toBe( "https://github.com/Wei-Shaw/sub2api/blob/main/docs/PAYMENT_CN.md", ); expect(paymentLinks[1]?.attributes("href")).toBe( "https://github.com/Wei-Shaw/sub2api/blob/main/docs/PAYMENT_CN.md#支持的支付方式", ); for (const link of paymentLinks) { expect(link.attributes("href")).toContain("docs/PAYMENT"); } }); it("does not submit legacy visible payment method settings", async () => { const wrapper = mountView(); await flushPromises(); await openPaymentTab(wrapper); await wrapper.find("form").trigger("submit.prevent"); await flushPromises(); expect(updateSettings).toHaveBeenCalledTimes(1); const payload = updateSettings.mock.calls[0]?.[0]; expect(payload).not.toHaveProperty("payment_visible_method_alipay_source"); expect(payload).not.toHaveProperty("payment_visible_method_wxpay_source"); expect(payload).not.toHaveProperty("payment_visible_method_alipay_enabled"); expect(payload).not.toHaveProperty("payment_visible_method_wxpay_enabled"); }); it("updates provider enablement immediately and reloads providers", async () => { const provider = { id: 7, provider_key: "alipay", name: "Official Alipay", config: {}, supported_types: ["alipay"], enabled: false, payment_mode: "", refund_enabled: false, allow_user_refund: false, limits: "", sort_order: 0, }; getProviders.mockReset(); getProviders .mockResolvedValueOnce({ data: [provider] }) .mockResolvedValueOnce({ data: [{ ...provider, enabled: true }] }); updateProvider.mockResolvedValue({ data: { ...provider, enabled: true } }); const PaymentProviderListStub = defineComponent({ emits: ["toggleField"], setup(_, { emit }) { return () => h( "button", { class: "provider-toggle-stub", onClick: () => emit("toggleField", provider, "enabled"), }, "toggle provider", ); }, }); const wrapper = mount(SettingsView, { global: { stubs: { AppLayout: AppLayoutStub, Select: SelectStub, Toggle: ToggleStub, Icon: true, ConfirmDialog: true, PaymentProviderList: PaymentProviderListStub, PaymentProviderDialog: true, GroupBadge: true, GroupOptionItem: true, ProxySelector: true, ImageUpload: ImageUploadStub, BackupSettings: true, }, }, }); await flushPromises(); await openPaymentTab(wrapper); await wrapper.get(".provider-toggle-stub").trigger("click"); await flushPromises(); expect(updateProvider).toHaveBeenCalledWith(7, { enabled: true }); expect(getProviders).toHaveBeenCalledTimes(2); }); 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 高级调度器"); }); it("passes translated upload and remove labels to the payment help image uploader", async () => { const wrapper = mountView(); await flushPromises(); await openPaymentTab(wrapper); const imageUploads = wrapper.findAll(".image-upload-stub"); expect(imageUploads.length).toBeGreaterThan(0); const paymentHelpImageUpload = imageUploads.find( (node) => node.attributes("data-placeholder") === "admin.settings.payment.helpImagePlaceholder", ); expect(paymentHelpImageUpload).toBeDefined(); expect(paymentHelpImageUpload?.attributes("data-upload-label")).toBe("上传图片"); expect(paymentHelpImageUpload?.attributes("data-remove-label")).toBe("移除"); }); }); 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(); updateProvider.mockReset(); createProvider.mockReset(); deleteProvider.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-mp-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-mp-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-mp-app-id"]') .setValue("wx-app-id-updated"); await wrapper .get('[data-testid="wechat-connect-mp-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_open_enabled: true, wechat_connect_mp_enabled: true, wechat_connect_mp_app_id: "wx-app-id-updated", wechat_connect_mp_app_secret: "new-secret", 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-mp-app-secret"]') .element as HTMLInputElement ).value, ).toBe(""); expect( wrapper .get('[data-testid="wechat-connect-mp-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("首次绑定时授权"); }); it("preserves optional OIDC compatibility flags instead of forcing them on save", async () => { getSettings.mockResolvedValueOnce({ ...baseSettingsResponse, oidc_connect_enabled: true, oidc_connect_use_pkce: false, oidc_connect_validate_id_token: false, }); const wrapper = mountView(); await flushPromises(); await openSecurityTab(wrapper); await wrapper.find("form").trigger("submit.prevent"); await flushPromises(); expect(updateSettings).toHaveBeenCalledTimes(1); expect(updateSettings).toHaveBeenCalledWith( expect.objectContaining({ oidc_connect_use_pkce: false, oidc_connect_validate_id_token: false, }), ); }); });