Files
lexgo/admin/tests/session.test.mjs

228 lines
11 KiB
JavaScript

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('creation and reset accept six bytes but reject five and over 72', async () => {
const writes = []
const { session } = setup(async (url, options) => {
if (url.endsWith('/login')) return response({ token: 'fictional-admin-token', user: admin })
writes.push(JSON.parse(options.body))
return response(learner)
})
await session.login(admin.username, 'fictional-password')
await assert.rejects(session.createAccount('fixture.new', 'z'.repeat(5)))
await assert.rejects(session.createAccount('fixture.new', 'z'.repeat(73)))
await session.createAccount('fixture.new', 'z'.repeat(6))
await session.createAccount('fixture.unicode', '虚构')
await session.createAccount('fixture.maximum', 'z'.repeat(72))
assert.equal(writes.length, 3)
await assert.rejects(session.updateAccount(learner.id, { password: 'z'.repeat(5) }))
await assert.rejects(session.updateAccount(learner.id, { password: 'z'.repeat(73) }))
for (const password of ['z'.repeat(6), '虚构', 'z'.repeat(72)]) {
await session.updateAccount(learner.id, { password })
assert.equal(writes.at(-1).password, password)
}
})
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)
})