import test from 'node:test' import assert from 'node:assert/strict' import { createSession, normalizeUsername, validPassword } from '../src/session.mjs' const admin = { id: 1, username: 'fixture.admin', role: 'admin' } const learner = { id: 2, username: 'fixture.learner', role: 'learner' } const response = (data, status = 200) => ({ ok: status < 400, status, json: async () => ({ code: status, data, msg: '请求失败' }) }) function setup(fetch) { const data = new Map() const storage = { getItem: key => data.get(key), setItem: (key, value) => data.set(key, value), removeItem: key => data.delete(key) } return { session: createSession({ fetch, storage }), data } } test('normalizes account names and enforces UTF-8 password byte limits', () => { assert.equal(normalizeUsername(' Fixture.Admin '), 'fixture.admin') assert.equal(validPassword('虚构测试密码'), true) assert.equal(validPassword('短'), false) assert.equal(validPassword('虚'.repeat(25)), false) }) test('learner login is rejected, revoked, and never stored in admin session', async () => { const requests = [] const { session, data } = setup(async (url, options) => { requests.push([url, options]) return response(url.endsWith('/login') ? { token: 'fictional-learner-token', user: learner } : null) }) await assert.rejects(session.login('fixture.learner', 'fictional-password'), /管理权限/) assert.equal(session.state.user, null) assert.equal(data.size, 0) assert.equal(requests[1][0], '/api/v1/logout') }) test('admin account list sends bearer and clears private data on 401', async () => { let expire = false const { session, data } = setup(async (url, options) => { if (url.endsWith('/login')) return response({ token: 'fictional-admin-token', user: admin }) assert.equal(options.headers.Authorization, 'Bearer fictional-admin-token') return expire ? response(null, 401) : response({ items: [learner] }) }) await session.login('fixture.admin', 'fictional-password') await session.loadAccounts() assert.deepEqual(session.state.accounts, [learner]) expire = true await assert.rejects(session.loadAccounts()) assert.equal(session.state.user, null) assert.deepEqual(session.state.accounts, []) assert.equal(data.size, 0) }) test('logout discards an in-flight account response', async () => { let complete const { session } = setup(async url => { if (url.endsWith('/login')) return response({ token: 'fictional-admin-token', user: admin }) if (url.endsWith('/accounts')) return new Promise(resolve => { complete = resolve }) return response(null) }) await session.login('fixture.admin', 'fictional-password') const pending = session.loadAccounts() await session.logout() complete(response({ items: [learner] })) await assert.rejects(pending, /会话已变化/) assert.deepEqual(session.state.accounts, []) }) test('account creation and state/password update use bounded API contract', async () => { const writes = [] const { session } = setup(async (url, options) => { if (url.endsWith('/login')) return response({ token: 'fictional-admin-token', user: admin }) writes.push([url, options.method, JSON.parse(options.body)]) return response(learner) }) await session.login('fixture.admin', 'fictional-password') await session.createAccount(' Fixture.Learner ', 'fictional-password') await session.updateAccount(2, { disabled: true }) await session.updateAccount(2, { password: 'fictional-new-password' }) assert.deepEqual(writes, [ ['/api/v1/accounts', 'POST', { username: 'fixture.learner', password: 'fictional-password' }], ['/api/v1/accounts/2', 'PATCH', { disabled: true }], ['/api/v1/accounts/2', 'PATCH', { password: 'fictional-new-password' }] ]) }) test('unauthenticated account access does not reach the API', async () => { const { session } = setup(async () => assert.fail('must not call API')) await assert.rejects(session.loadAccounts(), /请先登录/) }) test('old restore failure cannot clear a newly logged in admin', async () => { let complete const { session, data } = setup(async url => { if (url.endsWith('/me')) return new Promise(resolve => { complete = resolve }) if (url.endsWith('/login')) return response({ token: 'fictional-new-token', user: admin }) return response(null) }) session.state.token = 'fictional-old-token' const pending = session.restore() await session.login('fixture.admin', 'fictional-password') complete(response(null, 401)) await assert.rejects(pending) assert.equal(session.state.user?.id, admin.id) assert.equal(data.get('lexgo-admin-token'), 'fictional-new-token') }) test('restored learner session is revoked and removed', async () => { let revoked = false const { session } = setup(async url => { if (url.endsWith('/logout')) { revoked = true; return response(null) } return response(learner) }) session.state.token = 'fictional-learner-token' await assert.rejects(session.restore(), /管理权限/) assert.equal(session.state.user, null) assert.equal(session.state.token, '') assert.equal(revoked, true) }) test('duplicate account error remains actionable without ending the admin session', async () => { const { session } = setup(async url => url.endsWith('/login') ? response({ token: 'fictional-admin-token', user: admin }) : { ok: false, status: 409, json: async () => ({ code: 409, msg: '账号已存在' }) }) await session.login('fixture.admin', 'fictional-password') await assert.rejects(session.createAccount('fixture.learner', 'fictional-password'), /账号已存在/) assert.equal(session.state.user.id, admin.id) }) test('admin self-disable is blocked before issuing a request', async () => { const { session } = setup(async url => { assert.equal(url, '/api/v1/login') return response({ token: 'fictional-admin-token', user: admin }) }) await session.login('fixture.admin', 'fictional-password') await assert.rejects(session.updateAccount(1, { disabled: true }), /不能停用当前管理员/) }) test('logout at the response-to-store handoff cannot restore account data', async () => { const { session } = setup(async url => { if (url.endsWith('/login')) return response({ token: 'fictional-admin-token', user: admin }) return { ok: true, status: 200, json: async () => { queueMicrotask(() => queueMicrotask(() => session.clear())) return { code: 200, data: { items: [learner] } } } } }) await session.login('fixture.admin', 'fictional-password') await assert.rejects(session.loadAccounts(), /会话已变化/) assert.deepEqual(session.state.accounts, []) }) test('administrator password reset is blocked before issuing a write', async () => { const { session } = setup(async url => { assert.equal(url, '/api/v1/login') return response({ token: 'fictional-admin-token', user: admin }) }) await session.login('fixture.admin', 'fictional-password') await assert.rejects(session.updateAccount(1, { password: 'fictional-new-password' }), /管理员/) }) test('another administrator cannot be enabled, disabled, or reset from account management', async () => { const otherAdmin = { id: 3, username: 'fixture.otheradmin', role: 'admin', disabled: false } const { session } = setup(async url => { if (url.endsWith('/login')) return response({ token: 'fictional-admin-token', user: admin }) assert.equal(url, '/api/v1/accounts') return response({ items: [otherAdmin] }) }) await session.login('fixture.admin', 'fictional-password') await session.loadAccounts() for (const patch of [{ disabled: true }, { disabled: false }, { password: 'fictional-new-password' }]) { await assert.rejects(session.updateAccount(3, patch), /管理员/) } }) test('logout HTTP 503 clears local private state but reports unconfirmed server revocation', async () => { const { session, data } = setup(async url => { if (url.endsWith('/login')) return response({ token: 'fictional-admin-token', user: admin }) if (url.endsWith('/accounts')) return response({ items: [learner] }) return response(null, 503) }) await session.login('fixture.admin', 'fictional-password') await session.loadAccounts() await assert.rejects(session.logout(), /服务器尚未确认退出/) assert.equal(session.state.user, null) assert.deepEqual(session.state.accounts, []) assert.equal(data.size, 0) }) test('account switch stops on unconfirmed revocation of the previous session', async () => { let loginCount = 0 const { session, data } = setup(async url => { if (url.endsWith('/login')) { loginCount++ return response({ token: 'fictional-admin-token', user: admin }) } return response(null, 503) }) await session.login('fixture.admin', 'fictional-password') await assert.rejects(session.login('fixture.otheradmin', 'fictional-password'), /服务器尚未确认退出/) assert.equal(loginCount, 1) assert.equal(session.state.user, null) assert.equal(data.size, 0) }) test('logout HTTP 401 is accepted because the server session is already invalid', async () => { const { session } = setup(async url => url.endsWith('/login') ? response({ token: 'fictional-admin-token', user: admin }) : response(null, 401)) await session.login('fixture.admin', 'fictional-password') await session.logout() assert.equal(session.state.user, null) }) test('rejected learner login reports failed server revocation without storing credentials', async () => { const { session, data } = setup(async url => url.endsWith('/login') ? response({ token: 'fictional-learner-token', user: learner }) : response(null, 503)) await assert.rejects(session.login('fixture.learner', 'fictional-password'), /没有管理权限.*服务器尚未确认退出/) assert.equal(session.state.user, null) assert.equal(data.size, 0) })