This commit is contained in:
parent
3d912fe5d1
commit
c0ef89cdce
@ -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
|
||||
)
|
||||
|
||||
@ -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)
|
||||
})
|
||||
|
||||
@ -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"/)
|
||||
|
||||
@ -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, /撤销删除/)
|
||||
})
|
||||
|
||||
Loading…
Reference in New Issue
Block a user