import { test } from 'node:test' import assert from 'node:assert/strict' import { onResponseFulfilled, onResponseRejected } from '../src/shared/api/http.ts' import { KICK_NOTICE_KEY, KICKED_CODE, handleKicked, isKickedPayload, } from '../src/shared/auth/kick-handler.ts' function createStorage(initial: Record = {}) { const store = new Map(Object.entries(initial)) return { getItem: (key: string) => (store.has(key) ? (store.get(key) as string) : null), setItem: (key: string, value: string) => void store.set(key, String(value)), removeItem: (key: string) => void store.delete(key), has: (key: string) => store.has(key), } } function setupWindow(initial: Record = {}, pathname = '/home') { const localStorage = createStorage(initial) const assigned: string[] = [] const listeners = new Map void>>() const fakeWindow: Record = { localStorage, location: { pathname, search: '', assign: (url: string) => { assigned.push(url) }, }, addEventListener: (type: string, handler: () => void) => { const list = listeners.get(type) || [] list.push(handler) listeners.set(type, list) }, } const dispatch = (type: string) => { for (const handler of listeners.get(type) || []) handler() } ;(globalThis as Record).window = fakeWindow return { localStorage, assigned, dispatch, fakeWindow } } /** 登录态齐全的初始存储(含桌面端设备号,互踢下线不应清掉它) */ function loggedInStorage() { return { aiimage_auth_token: 'jwt-token', uid: '1095', username: 'ceshi001', aiimage_auto_login: '1', aiimage_device_id: 'dev-fingerprint', aiimage_remember_user: 'ceshi001', } } test('isKickedPayload 只认 code=4011', () => { assert.equal(isKickedPayload({ success: false, code: KICKED_CODE, message: '该账号已在其他设备登录' }), true) assert.equal(isKickedPayload({ success: false, code: 401, message: '未登录' }), false) assert.equal(isKickedPayload({ success: true, data: {} }), false) assert.equal(isKickedPayload(null), false) assert.equal(isKickedPayload('4011'), false) }) test('handleKicked 清登录态并关自动登录,跳登录页带 kicked 标记', () => { const { localStorage, assigned } = setupWindow(loggedInStorage()) handleKicked() assert.equal(localStorage.getItem('aiimage_auth_token'), null) assert.equal(localStorage.getItem('uid'), null) assert.equal(localStorage.getItem('username'), null) // 被踢后不再自动重登,否则本机每次启动都会静默重登把对方又顶下线 assert.equal(localStorage.getItem('aiimage_auto_login'), '0') assert.equal(localStorage.getItem(KICK_NOTICE_KEY), '1') // 设备号与记住的账号保留:同设备重登不算新设备,账号回填方便手动重登 assert.equal(localStorage.getItem('aiimage_device_id'), 'dev-fingerprint') assert.equal(localStorage.getItem('aiimage_remember_user'), 'ceshi001') assert.deepEqual(assigned, ['/login?kicked=1']) }) test('handleKicked 已在登录页时不跳转(仍清理登录态)', () => { const { localStorage, assigned } = setupWindow(loggedInStorage(), '/login') handleKicked() assert.equal(localStorage.getItem('uid'), null) assert.equal(localStorage.getItem(KICK_NOTICE_KEY), '1') assert.deepEqual(assigned, []) }) test('handleKicked 并发重复触发只跳转一次且状态一致', () => { const { localStorage, assigned } = setupWindow(loggedInStorage()) handleKicked() handleKicked() assert.equal(localStorage.getItem('uid'), null) assert.equal(localStorage.getItem('aiimage_auto_login'), '0') assert.deepEqual(assigned, ['/login?kicked=1']) }) test('handleKicked 桌面端同步清 Python 侧 current_uid', () => { const { localStorage, fakeWindow } = setupWindow(loggedInStorage()) const saved: Array> = [] fakeWindow.pywebview = { api: { save_config: (data: Record) => void saved.push(data) } } handleKicked() assert.deepEqual(saved, [{ current_uid: '' }]) }) test('handleKicked 桥未就绪时等 pywebviewready 补清 current_uid', () => { // 真机复现:被顶下线发生在页面刚加载时,pywebview 桥尚未就绪,立即调用会被丢弃 const { dispatch, fakeWindow } = setupWindow(loggedInStorage()) const saved: Array> = [] handleKicked() assert.deepEqual(saved, [], '桥未就绪时不应有调用结果') // 桥就绪:注入 api 并触发就绪事件,应补一次清理 fakeWindow.pywebview = { api: { save_config: (data: Record) => void saved.push(data) } } dispatch('pywebviewready') assert.deepEqual(saved, [{ current_uid: '' }]) }) test('handleKicked 网页形态(无 pywebview)不受影响', () => { const { localStorage, assigned } = setupWindow(loggedInStorage()) handleKicked() assert.equal(localStorage.getItem('uid'), null) assert.deepEqual(assigned, ['/login?kicked=1']) }) // ---------- 拦截器接线:Java 错误信封走 HTTP 200,成功分支也必须判 ---------- test('响应成功分支:body.code=4011 触发下线', () => { const { localStorage, assigned } = setupWindow(loggedInStorage()) const response = { data: { success: false, code: KICKED_CODE, message: '该账号已在其他设备登录,本设备已下线' } } const returned = onResponseFulfilled(response as never) assert.equal(returned, response) assert.equal(localStorage.getItem('uid'), null) assert.equal(localStorage.getItem('aiimage_auto_login'), '0') assert.deepEqual(assigned, ['/login?kicked=1']) }) test('响应失败分支:错误响应体 code=4011 触发下线', async () => { const { localStorage, assigned } = setupWindow(loggedInStorage()) await assert.rejects( () => onResponseRejected({ response: { data: { code: KICKED_CODE } }, message: '请求失败' }), /请求失败/, ) assert.equal(localStorage.getItem('uid'), null) assert.deepEqual(assigned, ['/login?kicked=1']) }) test('普通业务错误(403 等)不触发下线', () => { const { localStorage, assigned } = setupWindow(loggedInStorage()) onResponseFulfilled({ data: { success: false, code: 403, message: '需要管理员权限' } } as never) assert.equal(localStorage.getItem('uid'), '1095') assert.deepEqual(assigned, []) })