Files
2026-10-09 16:46:35 +08:00

141 lines
6.2 KiB
JavaScript

// Isolated navigation/authentication checks: no server or real device requests.
import assert from 'node:assert/strict'
import { test } from 'node:test'
import fs from 'node:fs'
import path from 'node:path'
import vm from 'node:vm'
import { fileURLToPath } from 'node:url'
import ts from 'typescript'
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../src')
function environment(pathname = '/dashboard') {
const storage = new Map(), redirects = [], cache = new Map()
const handlers = {}
const http = {
interceptors: {
request: { use: callback => { handlers.request = callback } },
response: { use: (success, failure) => { handlers.success = success; handlers.failure = failure } },
},
}
const window = { location: { pathname, replace: url => redirects.push(url) } }
const mocks = {
axios: { default: { create: () => http } },
'vue-router': {
createWebHistory: () => ({}),
createRouter: options => ({
options,
beforeEach: callback => { handlers.guard = callback },
afterEach: callback => { handlers.after = callback },
}),
},
}
function load(relative) {
const file = path.resolve(root, relative)
if (cache.has(file)) return cache.get(file).exports
const module = { exports: {} }; cache.set(file, module)
const source = ts.transpileModule(fs.readFileSync(file, 'utf8'), {
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS },
}).outputText
vm.runInNewContext(source, {
exports: module.exports, module, window,
localStorage: {
getItem: key => storage.get(key) ?? null,
setItem: (key, value) => storage.set(key, value),
removeItem: key => storage.delete(key),
},
require: name => mocks[name] ?? (name.endsWith('.vue') ? { default: {} }
: load(path.relative(root, path.resolve(path.dirname(file), `${name}.ts`)))),
}, { filename: file })
return module.exports
}
const auth = load('services/auth.ts')
load('services/http.ts')
return { auth, handlers, redirects, storage, load }
}
function request(env) {
return env.handlers.request({ headers: {} })
}
test('a current HTTP 401 clears both tokens and redirects only once', async () => {
const env = environment()
env.auth.saveTokens('session-one', 'refresh-one')
const config = request(env)
const fail = () => env.handlers.failure({ config, response: { status: 401 }, message: 'Unauthorized' })
await assert.rejects(fail(), /Unauthorized/)
await assert.rejects(fail(), /Unauthorized/)
assert.equal(env.auth.hasToken(), false)
assert.equal(env.storage.size, 0)
assert.deepEqual(env.redirects, ['/auth'])
})
test('an HTTP 200 with an unauthorized envelope also expires the current session', async () => {
const env = environment()
env.auth.saveTokens('session-one', 'refresh-one')
const config = request(env)
await assert.rejects(env.handlers.success({ config, data: { code: 401, type: 'error', result: null } }), /登录已过期/)
assert.equal(env.auth.hasToken(), false)
assert.deepEqual(env.redirects, ['/auth'])
})
test('a late HTTP/envelope 401 from the previous login cannot remove a newer session', async () => {
const env = environment()
env.auth.saveTokens('old-session', 'old-refresh')
const oldConfig = request(env)
env.auth.saveTokens('new-session', 'new-refresh')
await assert.rejects(env.handlers.failure({ config: oldConfig, response: { status: 401 }, message: 'Expired' }), /Expired/)
await assert.rejects(env.handlers.success({ config: oldConfig, data: { code: 401, message: 'Expired' } }), /Expired/)
assert.equal(env.auth.getToken(), 'new-session')
assert.equal(env.storage.size, 2)
assert.deepEqual(env.redirects, [])
})
test('an anonymous request that returns 401 cannot log out a later authenticated session', async () => {
const env = environment('/auth')
const anonymousConfig = request(env)
assert.equal(anonymousConfig.headers.Authorization, undefined)
env.auth.saveTokens('new-session', 'new-refresh')
await assert.rejects(env.handlers.failure({ config: anonymousConfig, response: { status: 401 }, message: 'Invalid password' }))
assert.equal(env.auth.getToken(), 'new-session')
assert.deepEqual(env.redirects, [])
})
test('failed credentials on the login page do not reload that page', async () => {
const env = environment('/auth')
await assert.rejects(env.handlers.failure({ config: request(env), response: { status: 401 }, message: 'Invalid password' }))
assert.deepEqual(env.redirects, [])
})
test('normal API results still unwrap and business failures are still rejected', async () => {
const env = environment()
env.auth.saveTokens('session-one', 'refresh-one')
assert.equal(request(env).headers.Authorization, 'Bearer session-one')
const result = { success: false, message: 'device unavailable' }
assert.equal(env.handlers.success({ data: { code: 200, type: 'success', result } }), result)
await assert.rejects(env.handlers.success({ data: { type: 'error', result: null, message: 'failed' } }), /failed/)
assert.equal(env.auth.hasToken(), true)
})
test('route guards protect pages while allowing login, with a fallback for unknown URLs', () => {
const env = environment(), router = env.load('router.ts').default
assert.equal(env.handlers.guard({ meta: {} }), '/auth')
assert.equal(env.handlers.guard({ meta: { guest: true } }), undefined)
env.auth.saveTokens('session-one', 'refresh-one')
assert.equal(env.handlers.guard({ meta: {} }), undefined)
assert.equal(env.handlers.guard({ meta: { guest: true } }), '/dashboard')
assert.equal(router.options.routes.at(-1).path, '/:pathMatch(.*)*')
assert.equal(router.options.routes.at(-1).redirect, '/dashboard')
const devices = router.options.routes.find(route => route.path === '/device-control')
assert.equal(devices.children.find(route => route.name === 'device-detail').path, ':deviceId')
})
test('new pages open at the top and browser back preserves its saved scroll position', () => {
const env = environment(), router = env.load('router.ts').default
const saved = { left: 0, top: 512 }
assert.equal(router.options.scrollBehavior({}, {}, saved), saved)
const fresh = router.options.scrollBehavior({}, {}, null)
assert.equal(fresh.left, 0)
assert.equal(fresh.top, 0)
})