fix(client): clear protected views on logout and show customer customs state
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
import ts from 'typescript'
|
||||
import * as vue from 'vue'
|
||||
|
||||
const layoutSource = readFileSync(new URL('./AppLayout.vue', import.meta.url), 'utf8')
|
||||
const shellStyles = readFileSync(new URL('../styles/shell.css', import.meta.url), 'utf8')
|
||||
@@ -108,7 +110,7 @@ test('source contract: locks body scrolling while open and restores it on close
|
||||
})
|
||||
|
||||
test('source contract: handles confirmation cancellation but rethrows unexpected logout errors', () => {
|
||||
assert.match(layoutSource, /async function handleLogout\(\) \{\s*try \{/)
|
||||
assert.match(layoutSource, /async function handleLogout\(\) \{\s*const logoutToken = auth.token\s*try \{/)
|
||||
assert.match(layoutSource, /\} catch \(error\) \{\s*if \(error === 'cancel' \|\| error === 'close'\) return\s*throw error\s*\}/)
|
||||
})
|
||||
|
||||
@@ -148,3 +150,49 @@ test('source contract: routes denied pages to an explicit 403 state', () => {
|
||||
assert.match(routerSource, /!auth\.hasPermission\(permission\)/)
|
||||
assert.match(routerSource, /getFirstAccessiblePath\(auth\.permissionCodes\)/)
|
||||
})
|
||||
|
||||
function logoutHarness(t) {
|
||||
const script = layoutSource.match(/<script setup lang="ts">([\s\S]*?)<\/script>/)[1]
|
||||
const ast = ts.createSourceFile('layout.ts', script, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
|
||||
const nodes = ast.statements.filter(node => (ts.isFunctionDeclaration(node) && node.name?.text === 'handleLogout') || (ts.isExpressionStatement(node) && node.getText(ast).startsWith('watch(') && node.getText(ast).includes('() => auth.isAuthenticated')))
|
||||
const code = ts.transpileModule(nodes.map(node => node.getText(ast)).join('\n'), { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText
|
||||
let rejectLogout, confirm
|
||||
const remote = new Promise((_, no) => { rejectLogout = no })
|
||||
const confirmation = new Promise(yes => { confirm = yes })
|
||||
const calls = [], warnings = []
|
||||
const auth = vue.reactive({ token: 'A', isAuthenticated: true, logout: () => { calls.push('logout'); auth.token = ''; auth.isAuthenticated = false; return remote } })
|
||||
const scope = vue.effectScope(); t.after(() => scope.stop())
|
||||
const handler = scope.run(() => new Function('watch', 'auth', 'workspace', 'unsaved', 'router', 'ElMessageBox', 'ElMessage', code + ';return handleLogout')(vue.watch, auth, { clear: () => calls.push('workspace') }, { hasDirty: false, clear: () => calls.push('unsaved') }, { replace: path => { calls.push(path); return Promise.resolve() } }, { confirm: () => confirmation }, { warning: value => warnings.push(value) }))
|
||||
return { auth, handler, confirm, rejectLogout, calls, warnings }
|
||||
}
|
||||
|
||||
test('logout navigates before remote completion and late failure cannot affect a new login', async t => {
|
||||
const h = logoutHarness(t)
|
||||
const clicked = h.handler(); h.confirm(); await clicked; await vue.nextTick()
|
||||
assert.deepEqual(h.calls, ['logout', 'workspace', 'unsaved', '/login'])
|
||||
assert.equal(h.auth.isAuthenticated, false)
|
||||
h.auth.token = 'B'; h.auth.isAuthenticated = true; await vue.nextTick()
|
||||
h.rejectLogout(new Error('SESSION_CHANGED')); await Promise.resolve(); await vue.nextTick()
|
||||
assert.equal(h.auth.token, 'B'); assert.equal(h.auth.isAuthenticated, true)
|
||||
assert.equal(h.calls.filter(item => item === '/login').length, 1)
|
||||
assert.equal(h.warnings.length, 0)
|
||||
})
|
||||
|
||||
test('cross-tab logout leaves protected route and stale confirmation never logs out a new identity', async t => {
|
||||
const h = logoutHarness(t)
|
||||
const clicked = h.handler()
|
||||
h.auth.token = 'B'; h.confirm(); await clicked
|
||||
assert.equal(h.calls.length, 0)
|
||||
h.auth.token = ''; h.auth.isAuthenticated = false; await vue.nextTick()
|
||||
assert.deepEqual(h.calls, ['workspace', 'unsaved', '/login'])
|
||||
assert.match(layoutSource, /<div v-if="auth.isAuthenticated" class="client-shell"/)
|
||||
})
|
||||
|
||||
test('remote logout failure reports incomplete server revocation without leaving protected content visible', async t => {
|
||||
const h = logoutHarness(t)
|
||||
const clicked = h.handler(); h.confirm(); await clicked; await vue.nextTick()
|
||||
h.rejectLogout(new Error('offline')); await Promise.resolve()
|
||||
assert.equal(h.auth.isAuthenticated, false)
|
||||
assert.ok(h.calls.includes('/login'))
|
||||
assert.deepEqual(h.warnings, ['已退出本页,服务器会话注销未完成'])
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user