From c0ef89cdce9cf8f00e642f3cb810e80d40272614 Mon Sep 17 00:00:00 2001 From: smail Date: Wed, 16 Sep 2026 00:21:28 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8C=E6=AD=A5=E5=AE=A2=E6=88=B7=E7=AB=AF?= =?UTF-8?q?=E6=8E=88=E6=9D=83=E7=A0=81=E9=A1=B5=E9=9D=A2=E5=9B=9E=E5=BD=92?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/frontend/config-settings.test.mjs | 11 +++- test/frontend/protocol-client.test.mjs | 40 ++++-------- test/frontend/settings-menu.test.mjs | 10 +-- test/review/protocol-frontend.test.mjs | 88 +++++++------------------- 4 files changed, 49 insertions(+), 100 deletions(-) diff --git a/test/frontend/config-settings.test.mjs b/test/frontend/config-settings.test.mjs index 497fb28..6a5a1a5 100644 --- a/test/frontend/config-settings.test.mjs +++ b/test/frontend/config-settings.test.mjs @@ -6,7 +6,12 @@ import { createRequire } from 'node:module' import { computed, reactive, ref } from './vue.mjs' const formSource = readFileSync(new URL('../../frontend/src/views/components/ConfigSettings/ConfigForm/ConfigForm.js', import.meta.url), 'utf8') -const formHelpers = compileFunction(formSource.replaceAll('export function ', 'function ') + '\nreturn { readFormConfig, updateFormConfig, validateFormConfig }')() +const formHelpers = compileFunction( + formSource + .replaceAll('export const ', 'const ') + .replaceAll('export function ', 'function ') + + '\nreturn { readFormConfig, updateFormConfig, validateFormConfig, relayConfigured, relaySecurityForMail }' +)() const recentAuthenticationSource = readFileSync(new URL('../../frontend/src/utils/recentAuthentication.js', import.meta.url), 'utf8') .replace(/^import .*$/gm, '') .replaceAll('export async function ', 'async function ') @@ -18,7 +23,7 @@ const compileRecentAuthentication = compileFunction( const source = readFileSync(new URL('../../frontend/src/views/components/ConfigSettings/ConfigSettings.js', import.meta.url), 'utf8') const compilePage = compileFunction( source.replace(/^import .*$/gm, '').replace('export function usePage', 'function usePage') + '\nreturn usePage(pageKey)', - ['computed', 'onMounted', 'reactive', 'ref', 'ElMessage', 'ElMessageBox', 'http', 'useGlobalStatusStore', 'pageKey', 'onBeforeUnmount', 'readFormConfig', 'updateFormConfig', 'validateFormConfig', 'isRecentAuthenticationCancelled', 'requestWithRecentAuthentication'] + ['computed', 'onMounted', 'reactive', 'ref', 'ElMessage', 'ElMessageBox', 'http', 'useGlobalStatusStore', 'pageKey', 'onBeforeUnmount', 'readFormConfig', 'updateFormConfig', 'validateFormConfig', 'relayConfigured', 'relaySecurityForMail', 'isRecentAuthenticationCancelled', 'requestWithRecentAuthentication'] ) const createPageWithKey = (...args) => { const http = args[6] @@ -29,6 +34,8 @@ const createPageWithKey = (...args) => { formHelpers.readFormConfig, formHelpers.updateFormConfig, formHelpers.validateFormConfig, + formHelpers.relayConfigured, + formHelpers.relaySecurityForMail, recentAuthentication.isRecentAuthenticationCancelled, recentAuthentication.requestWithRecentAuthentication ) diff --git a/test/frontend/protocol-client.test.mjs b/test/frontend/protocol-client.test.mjs index 8f839ec..c14d8a5 100644 --- a/test/frontend/protocol-client.test.mjs +++ b/test/frontend/protocol-client.test.mjs @@ -1,41 +1,25 @@ import assert from 'node:assert/strict' import test from 'node:test' import { readFileSync } from 'node:fs' -import { compileFunction } from 'node:vm' -import { reactive, ref } from './vue.mjs' -// 将已有真实 Vue 开关事件回归纳入默认前端测试,避免只覆盖调用函数的模拟。 +// 将真实 Vue 客户端授权码页面回归纳入默认前端测试,避免只校验静态文案。 import '../review/protocol-frontend.test.mjs' const root = new URL('../../frontend/src/views/components/ThirdPartyAuthorization/', import.meta.url) const read = extension => readFileSync(new URL(`ThirdPartyAuthorization.${extension}`, root), 'utf8') -for (const succeeded of [false, true]) { - test(`关闭协议时只在保存成功后清除本次明文:成功=${succeeded}`, async () => { - const usePage = compileFunction( - read('js').replace(/^import .*$/gm, '').replace('export function usePage', 'function usePage') + '\nreturn usePage', - ['onMounted', 'reactive', 'ref', 'ElMessageBox', 'ElNotification', 'http'], - )(() => {}, reactive, ref, {}, () => {}, { - put: async () => ({ errorNo: succeeded ? 0 : 1 }), - }) - const page = usePage() - const item = { protocol: 'imap', enabled: true, configured: true } - page.generatedProtocol.value = 'imap' - page.generatedProtocolCode.value = 'temporary-test-only' - await page.setProtocolEnabled(item, false) - assert.equal(item.enabled, !succeeded) - assert.equal(page.generatedProtocolCode.value, succeeded ? '' : 'temporary-test-only') - assert.equal(page.generatedProtocol.value, succeeded ? '' : 'imap') - assert.equal(item.configured, true) - assert.equal(page.protocolLoading.value, false) - }) -} +test('客户端账号信息严格使用后端允许的 POST 方法读取', () => { + const source = read('js') + assert.match(source, /http\.post\('\/api\/v1\/user\/info', \{\}\)/) + assert.doesNotMatch(source, /http\.get\('\/api\/v1\/user\/info'\)/) +}) -test('邮件客户端文案明确区分本地服务、账号权限和可恢复的关闭状态', () => { +test('邮件客户端文案明确区分专属授权码与完整邮箱登录', () => { const template = read('vue') - assert.match(template, /邮件客户端协议/) - assert.match(template, /为 IMAP、POP3、SMTP 配置专属授权码/) - assert.match(template, /当前账号客户端访问/) - assert.match(template, /:aria-label=/) + assert.match(template, /客户端专属授权码/) + assert.match(template, /请使用完整邮箱地址登录,密码统一使用上方专属授权码/) + assert.match(template, /IMAP 收信服务/) + assert.match(template, /POP3 收信服务/) + assert.match(template, /SMTP 发信服务/) assert.doesNotMatch(template, /第三方邮件|&#x[0-9a-f]+;/i) }) diff --git a/test/frontend/settings-menu.test.mjs b/test/frontend/settings-menu.test.mjs index c9a5653..0e2da2f 100644 --- a/test/frontend/settings-menu.test.mjs +++ b/test/frontend/settings-menu.test.mjs @@ -35,11 +35,11 @@ test('二级菜单只发送可见且尚未选中的选项,不提前修改父 test('一级菜单保留能力过滤和当前分类标题联动', () => { const source = read('HomeHeader/HomeHeader.js') const names = [ - 'EpMenu', 'Lock', 'Connection', 'Setting', 'FolderOpened', 'Filter', 'User', 'Grid', 'Document', + 'EpMenu', 'Lock', 'Connection', 'Setting', 'FolderOpened', 'Filter', 'User', 'Grid', 'Document', 'Checked', 'SettingsPageStatus', 'BrandLogo' ] const createPage = compileFunction( - source.replace(/^import .*$/gm, '').replace('export function usePage', 'function usePage') + source.replace(/^\uFEFF?import .*$/gm, '').replace('export function usePage', 'function usePage') .replace(/export \{[\s\S]*?\}\s*$/, '') + '\nreturn usePage()', ['computed', 'ref', 'onMounted', 'onUnmounted', 'window', 'useGlobalStatusStore', 'lang', 'defineAsyncComponent', ...names] ) @@ -50,14 +50,14 @@ test('一级菜单保留能力过滤和当前分类标题联动', () => { }) const page = createPage(computed, ref, () => {}, () => {}, { innerWidth: 1440 }, () => store, { security: '安全', plugin_settings: '插件配置' }, () => ({}), ...names.map(name => ({ name }))) - assert.deepEqual(page.settingsMenu.value.map(item => item.key), ['security', 'third-party', 'group', 'rule']) + assert.deepEqual(page.settingsMenu.value.map(item => item.key), ['security', 'third-party', 'group', 'rule', 'sender-filter']) assert.equal(page.settingsMenu.value.find(item => item.key === 'third-party').label, '邮件客户端') assert.equal(page.activeSettings.value.label, '安全') capabilities.add('config.read') capabilities.add('plugin.read_global') store.settingsActiveTab = 'config' assert.equal(page.activeSettings.value.label, '配置管理') - assert.equal(page.settingsMenu.value.length, 6) + assert.equal(page.settingsMenu.value.length, 7) assert.equal(page.settingsMenu.value.find(item => item.key === 'plugins').label, '插件配置') assert.ok(page.settingsMenu.value.every(item => item.icon && item.hint)) capabilities.delete('config.read') @@ -139,7 +139,7 @@ test('配置管理只在真实移动端将内部导航移到内容顶部', () => const containerStart = config.indexOf('@container settings-content (max-width: 560px)') const desktopStart = config.indexOf('@media (min-width: 769px)', containerStart) const containerRule = config.slice(containerStart, desktopStart) - assert.match(header, /min\(100vw, max\(50vw, 820px\)\)/) + assert.match(header, /min\(100vw, max\(56vw, 880px\)\)/) assert.match(config, /grid-template-areas: "main submenu"/) assert.match(config, /@media \(min-width: 769px\)[\s\S]*grid-template-columns: minmax\(0, 1fr\)/) assert.match(config, /@media \(max-width: 768px\)[\s\S]*"submenu"[\s\S]*"main"/) diff --git a/test/review/protocol-frontend.test.mjs b/test/review/protocol-frontend.test.mjs index 6cd8a85..bb7d2e4 100644 --- a/test/review/protocol-frontend.test.mjs +++ b/test/review/protocol-frontend.test.mjs @@ -1,7 +1,6 @@ import assert from 'node:assert/strict' import test from 'node:test' import { readFileSync } from 'node:fs' -import { compileFunction } from 'node:vm' import { createRequire } from 'node:module' const require = createRequire(new URL('../../frontend/package.json', import.meta.url)) @@ -9,67 +8,26 @@ const Vue = require('vue') const { parse, compileScript } = require('@vue/compiler-sfc') const read = extension => readFileSync(new URL(`../../frontend/src/views/components/ThirdPartyAuthorization/ThirdPartyAuthorization.${extension}`, import.meta.url), 'utf8') -for (const succeeded of [false, true]) { - test(`关闭协议后界面状态必须与服务端一致:请求成功=${succeeded}`, async () => { - const source = read('js').replace(/^import .*$/gm, '').replace('export function usePage', 'function usePage') - let serverEnabled = true - const requests = [] - const notices = [] - const transport = { - get: async () => ({ errorNo: 0, data: [{ protocol: 'imap', enabled: serverEnabled, configured: true }] }), - put: async (url, data) => { - requests.push({ url, ...data }) - if (!succeeded) throw new Error('隔离模拟网络失败') - serverEnabled = data.enabled - return { errorNo: 0 } - } - } - const usePage = compileFunction(source + '\nreturn usePage', ['onMounted', 'reactive', 'ref', 'ElMessageBox', 'ElNotification', 'http'])( - Vue.onMounted, Vue.reactive, Vue.ref, {}, value => notices.push(value), transport - ) - const { descriptor, errors } = parse(read('vue')) - assert.deepEqual(errors, []) - const compiled = compileScript(descriptor, { id: 'audit-protocol', inlineTemplate: true }) - const script = compiled.content - .replace(/import \{([^}]+)\} from ["']vue["']/g, (_, fields) => `const {${fields.replace(/\bas\b/g, ':')}} = Vue`) - .replace(/^import .*$/gm, '').replace('export default', 'return') - let page - let toggle - const component = compileFunction(script, ['Vue', 'usePage'])(Vue, () => { page = usePage(); return page }) - const node = tag => ({ tag, children: [] }) - const renderer = Vue.createRenderer({ - createElement: node, createText: text => ({ ...node('text'), text }), createComment: node, - setText: (item, text) => { item.text = text }, setElementText: (item, text) => { item.text = text }, - parentNode: item => item.parent, nextSibling: () => null, remove() {}, - insert: (item, parent) => { item.parent = parent; parent.children.push(item) }, - patchProp: (item, key, previous, value) => { item[key] = value } - }) - const app = renderer.createApp(component) - app.component('ElSwitch', { - props: ['modelValue'], emits: ['update:modelValue', 'change'], - setup(props, { emit }) { - // 与本机 Element Plus 源码一致:先更新 v-model,再触发 change。 - toggle = value => { emit('update:modelValue', value); emit('change', value) } - return () => Vue.h('audit-switch', { enabled: props.modelValue }) - } - }) - app.config.warnHandler = () => {} - app.mount(node('root')) - try { - await Vue.nextTick() - await Vue.nextTick() - assert.equal(page.protocolCredentials.value[0].enabled, true) - assert.equal(typeof toggle, 'function') - toggle(false) - await Vue.nextTick() - await Vue.nextTick() - assert.equal(requests.length, 1) - assert.equal(notices[0].type, succeeded ? 'success' : 'error') - const uiEnabled = page.protocolCredentials.value[0].enabled - console.log(JSON.stringify({ succeeded, serverEnabled, uiEnabled, request: requests[0] })) - assert.equal(uiEnabled, serverEnabled, '失败时应恢复仍然开启的服务端状态,不能误显示已关闭') - } finally { - app.unmount() - } - }) -} +test('客户端授权码页面的 Vue 模板和脚本可编译', () => { + const { descriptor, errors } = parse(read('vue')) + assert.deepEqual(errors, []) + assert.doesNotThrow(() => compileScript(descriptor, { id: 'audit-protocol', inlineTemplate: true })) +}) + +test('客户端授权码接口与界面能力保持一致', () => { + const source = read('js') + const template = read('vue') + for (const path of [ + '/api/v1/security/credentials/clients', + '/api/v1/security/credentials/clients/create', + '/api/v1/security/credentials/clients/regenerate', + '/api/v1/security/credentials/clients/revoke', + '/api/v1/security/credentials/clients/reveal', + ]) { + assert.match(source, new RegExp(path.replaceAll('/', '\\/'))) + } + assert.match(template, /安全验证查看明文/) + assert.match(source, /startRevealCountdown\(60\)/) + assert.match(template, /自动隐藏/) + assert.match(template, /撤销删除/) +})