199 lines
8.9 KiB
JavaScript
199 lines
8.9 KiB
JavaScript
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'
|
|
import { computed, reactive, ref } from './vue.mjs'
|
|
|
|
const source = readFileSync(new URL('../../frontend/src/views/components/ConfigSettings/ConfigSettings.js', import.meta.url), 'utf8')
|
|
const createPage = compileFunction(
|
|
source.replace(/^import .*$/gm, '').replace('export function usePage', 'function usePage') + '\nreturn usePage()',
|
|
['computed', 'onMounted', 'reactive', 'ref', 'ElMessage', 'ElMessageBox', 'http', 'useGlobalStatusStore']
|
|
)
|
|
|
|
function fixture(get) {
|
|
const messages = []
|
|
const page = createPage(computed, () => {}, reactive, ref,
|
|
{ error: message => messages.push(message) }, {}, { get }, () => ({ hasCapability: () => true }))
|
|
return { page, messages }
|
|
}
|
|
|
|
function deferred() {
|
|
let resolve
|
|
let reject
|
|
const promise = new Promise((success, failure) => { resolve = success; reject = failure })
|
|
return { promise, resolve, reject }
|
|
}
|
|
|
|
test('旧版配置仅请求状态,不并发请求分区、备份和审计', async () => {
|
|
const requests = []
|
|
const { page, messages } = fixture(async path => {
|
|
requests.push(path)
|
|
return { errorNo: 0, data: { available: false, unavailableReason: '在线配置管理尚未启用' } }
|
|
})
|
|
await page.loadAll()
|
|
assert.deepEqual(requests, ['/api/v1/config/status'])
|
|
assert.equal(page.availabilityError.value, '在线配置管理尚未启用')
|
|
assert.deepEqual(messages, [])
|
|
assert.equal(page.loading.value, false)
|
|
})
|
|
|
|
test('备份和审计失败只显示局部错误,不阻挡配置编辑区域', async () => {
|
|
const { page, messages } = fixture(async path => {
|
|
if (path.endsWith('/status')) return { errorNo: 0, data: { available: true } }
|
|
if (path.endsWith('/base')) return { errorNo: 0, data: {} }
|
|
throw new Error('配置服务暂时不可用')
|
|
})
|
|
await page.loadAll()
|
|
assert.equal(page.availabilityError.value, '')
|
|
assert.equal(page.editorText.value, '{}')
|
|
assert.equal(page.backupsError.value, '配置服务暂时不可用')
|
|
assert.equal(page.auditError.value, '配置服务暂时不可用')
|
|
assert.deepEqual(messages, [])
|
|
await page.loadBackups()
|
|
assert.deepEqual(messages, [])
|
|
assert.equal(page.backupsError.value, '配置服务暂时不可用')
|
|
})
|
|
|
|
test('附属模块失败清除旧数据和回滚选择,重试成功清除错误', async () => {
|
|
let fail = true
|
|
const { page } = fixture(async () => {
|
|
if (fail) throw new Error('网络不可用')
|
|
return { errorNo: 0, data: [{ generation: 2 }] }
|
|
})
|
|
page.backups.value = [{ generation: 1 }]
|
|
page.auditItems.value = [{ id: 'old' }]
|
|
page.rollbackGeneration.value = 1
|
|
page.rollbackPreview.value = { targetGeneration: 1 }
|
|
page.rollbackConfirmed.value = true
|
|
await Promise.all([page.loadBackups(), page.loadAudit()])
|
|
assert.deepEqual(page.backups.value, [])
|
|
assert.deepEqual(page.auditItems.value, [])
|
|
assert.equal(page.rollbackGeneration.value, 0)
|
|
assert.equal(page.rollbackPreview.value, null)
|
|
assert.equal(page.rollbackConfirmed.value, false)
|
|
fail = false
|
|
await Promise.all([page.loadBackups(), page.loadAudit()])
|
|
assert.equal(page.backupsError.value, '')
|
|
assert.equal(page.auditError.value, '')
|
|
assert.equal(page.backups.value.length, 1)
|
|
assert.equal(page.auditItems.value.length, 1)
|
|
assert.equal(page.backupsLoading.value, false)
|
|
assert.equal(page.auditLoading.value, false)
|
|
})
|
|
|
|
test('状态请求失败后可重试恢复,不保留不可用提示', async () => {
|
|
let available = false
|
|
const { page } = fixture(async path => {
|
|
if (!available) throw new Error('网络不可用')
|
|
return { errorNo: 0, data: path.endsWith('/status') ? { available: true } : {} }
|
|
})
|
|
await page.loadAll()
|
|
assert.equal(page.availabilityError.value, '网络不可用')
|
|
available = true
|
|
await page.loadAll()
|
|
assert.equal(page.availabilityError.value, '')
|
|
})
|
|
|
|
test('快速切换分区时丢弃较慢的旧响应,并清除旧编辑内容与确认状态', async () => {
|
|
const base = deferred()
|
|
const email = deferred()
|
|
const { page } = fixture(path => path.endsWith('/base') ? base.promise : email.promise)
|
|
page.editorText.value = '{"old":true}'
|
|
page.confirmed.value = true
|
|
page.secretValue.value = 'test-only-value'
|
|
const baseRequest = page.loadSection()
|
|
assert.equal(page.editorText.value, '')
|
|
assert.equal(page.confirmed.value, false)
|
|
assert.equal(page.secretValue.value, '')
|
|
assert.equal(page.sectionLoading.value, true)
|
|
const emailRequest = page.selectSection('email')
|
|
email.resolve({ errorNo: 0, data: { policy: { smtpPort: 25 } } })
|
|
await emailRequest
|
|
assert.equal(page.currentSection.value.label, '邮箱策略')
|
|
assert.equal(page.sectionLoading.value, false)
|
|
base.resolve({ errorNo: 0, data: { database: { type: 'sqlite' } } })
|
|
await baseRequest
|
|
assert.deepEqual(JSON.parse(page.editorText.value), { policy: { smtpPort: 25 } })
|
|
assert.equal(page.databaseConfig.type, '')
|
|
})
|
|
|
|
test('过期请求失败不清除新分区的加载状态或覆盖错误信息', async () => {
|
|
const base = deferred()
|
|
const ssl = deferred()
|
|
const { page } = fixture(path => path.endsWith('/base') ? base.promise : ssl.promise)
|
|
const baseRequest = page.loadSection()
|
|
const sslRequest = page.selectSection('ssl')
|
|
base.reject(new Error('旧分区读取失败'))
|
|
await baseRequest
|
|
assert.equal(page.sectionLoading.value, true)
|
|
assert.equal(page.sectionError.value, '')
|
|
ssl.resolve({ errorNo: 0, data: { enabled: true } })
|
|
await sslRequest
|
|
assert.equal(page.sectionLoading.value, false)
|
|
assert.equal(page.sectionError.value, '')
|
|
assert.deepEqual(JSON.parse(page.editorText.value), { enabled: true })
|
|
})
|
|
|
|
test('变更执行期间或分区值无效时不切换,所有分区均提供中文说明', async () => {
|
|
const requests = []
|
|
const { page } = fixture(async path => { requests.push(path); return { errorNo: 0, data: {} } })
|
|
page.working.value = 'save'
|
|
await page.selectSection('smtp')
|
|
assert.equal(page.selectedSection.value, 'base')
|
|
page.working.value = ''
|
|
await page.selectSection('invalid-section')
|
|
await page.selectSection('base')
|
|
assert.deepEqual(requests, [])
|
|
for (const section of page.sectionOptions) {
|
|
assert.match(section.description, /[\u4e00-\u9fff]/)
|
|
}
|
|
await page.selectSection('plugins')
|
|
assert.deepEqual(requests, ['/api/v1/config/plugins'])
|
|
assert.equal(page.currentSection.value.label, '插件配置')
|
|
})
|
|
|
|
test('迁移预览票据返回模板,迁移执行取消时不发送写入请求', async () => {
|
|
const requests = []
|
|
const page = createPage(computed, () => {}, reactive, ref,
|
|
{ success: () => {}, error: () => assert.fail('不应显示错误') },
|
|
{ confirm: async () => { throw 'cancel' } },
|
|
{ post: async (path, payload) => {
|
|
requests.push({ path, payload })
|
|
return { errorNo: 0, data: { migration: { id: 'test-migration', status: 'previewed' }, token: 'test-only-ticket' } }
|
|
} }, () => ({ hasCapability: () => true }))
|
|
page.status.activeGeneration = 3
|
|
page.migrationTargetName.value = ' test-next.db '
|
|
assert.equal(page.migrationToken.value, '')
|
|
await page.previewDatabaseMigration()
|
|
assert.equal(page.migrationToken.value, 'test-only-ticket')
|
|
assert.equal(page.migrationPreview.value.id, 'test-migration')
|
|
await page.executeDatabaseMigration()
|
|
assert.deepEqual(requests, [{
|
|
path: '/api/v1/database/migrations/preview',
|
|
payload: { targetName: 'test-next.db', expectedGeneration: 3 }
|
|
}])
|
|
})
|
|
|
|
test('配置模板保留权限与确认绑定,维护操作使用原生可访问折叠面板', () => {
|
|
const require = createRequire(new URL('../../frontend/package.json', import.meta.url))
|
|
const { parse, compileScript, compileStyle } = require('@vue/compiler-sfc')
|
|
const root = new URL('../../frontend/src/views/components/ConfigSettings/', import.meta.url)
|
|
const template = readFileSync(new URL('ConfigSettings.vue', root), 'utf8')
|
|
const css = readFileSync(new URL('ConfigSettings.css', root), 'utf8')
|
|
const { descriptor, errors } = parse(template)
|
|
assert.deepEqual(errors, [])
|
|
assert.doesNotThrow(() => compileScript(descriptor, { id: 'config-layout', inlineTemplate: true }))
|
|
assert.deepEqual(compileStyle({ source: css, id: 'data-v-config-layout', scoped: true }).errors, [])
|
|
assert.match(template, /v-if="canWrite" class="editor-footer"/)
|
|
assert.match(template, /v-model="confirmed"/)
|
|
assert.match(template, /<details v-if="canMigrate && selectedSection === 'base'"/)
|
|
assert.match(template, /<details v-if="canRestart && status.pendingRestart\?\.length"[^>]+ open>/)
|
|
assert.match(template, /<details v-if="canRollback"/)
|
|
assert.match(template, /<details v-if="canAudit"/)
|
|
assert.equal((template.match(/<summary class="maintenance-summary">/g) || []).length, 4)
|
|
assert.match(css, /@container config-main \(max-width: 620px\)/)
|
|
assert.match(css, /\.maintenance-summary:focus-visible/)
|
|
assert.doesNotMatch(template, /&#x[0-9a-f]+;/i)
|
|
})
|