feat(selfhost): show sanitized update logs in admin UI
This commit is contained in:
@@ -99,3 +99,90 @@ test('an older polling response cannot erase a new submission whose response was
|
||||
assert.equal(f.ui.pending.value.body.requestId, id)
|
||||
assert.equal(f.ui.canApply.value, false)
|
||||
})
|
||||
|
||||
test('failed jobs automatically show logs and refresh them only while open', async t => {
|
||||
const failed = { status: 'failed', command: 'apply', requestId: 'failed-request', error: { code: 'UPDATE_FAILED', message: 'Auftrag fehlgeschlagen.' } }
|
||||
let content = 'pull access denied'
|
||||
const f = await setup(t, async path => {
|
||||
if (path.endsWith('/logs')) return { requestId: failed.requestId, logs: [{ source: 'commands', content, truncated: false }] }
|
||||
return path.endsWith('/status') ? { ...overview(failed), capabilities: { requestLogs: true } } : failed
|
||||
})
|
||||
assert.equal(f.ui.logsOpen.value, true)
|
||||
assert.equal(f.ui.logEntries.value[0].content, content)
|
||||
content = 'final failure detail'
|
||||
await f.ui.refresh()
|
||||
assert.equal(f.ui.logEntries.value[0].content, content)
|
||||
await f.ui.toggleLogs()
|
||||
const count = f.calls.filter(c => c.path.endsWith('/logs')).length
|
||||
await f.ui.refresh()
|
||||
assert.equal(f.ui.logsOpen.value, false)
|
||||
assert.equal(f.calls.filter(c => c.path.endsWith('/logs')).length, count)
|
||||
})
|
||||
|
||||
test('older host services explain the required upgrade without requesting unsupported endpoints', async t => {
|
||||
const failed = { status: 'failed', command: 'apply', requestId: 'failed-request' }
|
||||
const f = await setup(t, async path => path.endsWith('/status') ? overview(failed) : failed)
|
||||
assert.equal(f.ui.logsOpen.value, true)
|
||||
assert.match(f.ui.logsMessage.value, /Host-Update-Dateien aktualisieren/)
|
||||
assert.equal(f.calls.some(c => c.path.endsWith('/logs')), false)
|
||||
assert.equal(f.ui.connected.value, true)
|
||||
})
|
||||
|
||||
test('log failures preserve job state and revoked admin access clears the displayed logs', async t => {
|
||||
let failure
|
||||
const f = await setup(t, async path => {
|
||||
if (path.endsWith('/logs')) {
|
||||
if (failure) throw failure
|
||||
return { requestId: check.requestId, logs: [{ source: 'commands', content: 'diagnostic', truncated: false }] }
|
||||
}
|
||||
return path.endsWith('/status') ? { ...overview(), capabilities: { requestLogs: true } } : check
|
||||
})
|
||||
await f.ui.toggleLogs()
|
||||
failure = { statusCode: 503 }
|
||||
await f.ui.refreshLogs()
|
||||
assert.match(f.ui.logsMessage.value, /konnte nicht geladen/)
|
||||
assert.equal(f.ui.connected.value, true)
|
||||
assert.equal(f.ui.job.value.status, 'checked')
|
||||
failure = { statusCode: 403 }
|
||||
await f.ui.refreshLogs()
|
||||
assert.equal(f.ui.accessDenied.value, true)
|
||||
assert.equal(f.ui.logEntries.value.length, 0)
|
||||
})
|
||||
|
||||
test('a delayed log response cannot appear under a newly submitted job', async t => {
|
||||
let release
|
||||
const f = await setup(t, async (path, options) => {
|
||||
if (options.method === 'POST') return { status: 'queued', requestId: options.body.requestId, command: 'check' }
|
||||
if (path.endsWith('/logs')) return new Promise(resolve => { release = resolve })
|
||||
return path.endsWith('/status') ? { ...overview(), capabilities: { requestLogs: true } } : check
|
||||
})
|
||||
const loading = f.ui.toggleLogs()
|
||||
await new Promise(setImmediate)
|
||||
await f.ui.check()
|
||||
release({ requestId: check.requestId, logs: [{ source: 'commands', content: 'previous job', truncated: false }] })
|
||||
await loading
|
||||
assert.notEqual(f.ui.job.value.requestId, check.requestId)
|
||||
assert.equal(f.ui.logEntries.value.length, 0)
|
||||
})
|
||||
|
||||
test('the update card renders log output as escaped text and explains truncation', async t => {
|
||||
const f = await setup(t, async path => path.endsWith('/status') ? overview() : check)
|
||||
f.ui.logsOpen.value = true
|
||||
const { parse, compileScript } = require('@vue/compiler-sfc')
|
||||
const descriptor = parse(fs.readFileSync(__dirname + '/../components/SelfhostUpdateCard.vue', 'utf8')).descriptor
|
||||
const compiled = compileScript(descriptor, { id: 'update-card-test', inlineTemplate: true }).content
|
||||
const exports = {}
|
||||
const context = vm.createContext({ exports, require, ref: vue.ref, computed: vue.computed,
|
||||
useRuntimeConfig: () => ({ public: {} }), useSelfhostUpdates: () => ({ ...f.ui,
|
||||
logEntries: vue.ref([{ source: 'commands', content: '<script>alert("log")</script>', truncated: true }]) }) })
|
||||
vm.runInContext(ts.transpile(compiled, { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS }), context)
|
||||
const app = vue.createSSRApp(exports.default)
|
||||
for (const name of ['UCard', 'UIcon', 'UBadge', 'UAlert', 'UProgress', 'UButton', 'UModal', 'UCheckbox']) {
|
||||
app.component(name, { setup: (_props, { slots }) => () => vue.h('div', slots.default?.()) })
|
||||
}
|
||||
const html = await require('@vue/server-renderer').renderToString(app)
|
||||
assert.match(html, /Werkzeugausgaben/)
|
||||
assert.match(html, /Gekürzte Anzeige/)
|
||||
assert.match(html, /<script>/)
|
||||
assert.doesNotMatch(html, /<script>/)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user