// 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) })