105 lines
5.0 KiB
JavaScript
105 lines
5.0 KiB
JavaScript
import test from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { createSession } from '../src/session.mjs'
|
|
import * as audit from '../src/audit-logs.mjs'
|
|
|
|
function setup() {
|
|
const pending = []
|
|
const session = createSession({
|
|
storage: { getItem() {}, setItem() {}, removeItem() {} },
|
|
fetch: (url, options) => new Promise(resolve => pending.push({ url, options, resolve }))
|
|
})
|
|
session.state.user = { id: 1, role: 'admin', username: 'fixture.admin' }
|
|
session.state.token = 'fictional-token'
|
|
return { session, pending }
|
|
}
|
|
const response = data => ({ ok: true, status: 200, json: async () => ({ code: 200, data }) })
|
|
|
|
test('unclassified rejected account updates have a readable action and filter', () => {
|
|
assert.equal(audit.actionLabels.update, '更新账号')
|
|
assert.equal(new URLSearchParams(audit.auditQuery('operation', { action: 'update' })).get('action'), 'update')
|
|
})
|
|
|
|
test('audit query normalizes exact username, preserves RFC3339 bounds and bounds pagination', async () => {
|
|
const { session, pending } = setup()
|
|
assert.equal(typeof session.queryAuditLogs, 'function')
|
|
const request = session.queryAuditLogs('operation', { username: ' Fixture.Admin ', result: 'failure', action: 'reset_password', page: 2, limit: 100, from: new Date('2026-09-10T00:00:00+08:00'), to: new Date('2026-09-11T00:00:00+08:00') })
|
|
const url = new URL(pending[0].url, 'https://fixture.invalid')
|
|
assert.equal(url.pathname, '/api/v1/operation-logs')
|
|
assert.deepEqual(Object.fromEntries(url.searchParams), { page: '2', limit: '100', username: 'fixture.admin', result: 'failure', action: 'reset_password', from: '2026-09-09T16:00:00.000Z', to: '2026-09-10T16:00:00.000Z' })
|
|
assert.equal(pending[0].options.cache, 'no-store')
|
|
pending[0].resolve(response({ items: [], total: 0, page: 2, limit: 100 }))
|
|
assert.equal((await request).total, 0)
|
|
for (const filter of [{ limit: 101 }, { page: 0 }, { result: 'unknown' }, { action: 'delete' }, { from: 'bad' }, { from: '2026-09-11', to: '2026-09-10' }]) {
|
|
await assert.rejects(session.queryAuditLogs('operation', filter))
|
|
}
|
|
assert.equal(pending.length, 1)
|
|
})
|
|
|
|
test('login query omits empty filters and rejects operation-only actions and unauthenticated access', async () => {
|
|
const { session, pending } = setup()
|
|
assert.equal(typeof session.queryAuditLogs, 'function')
|
|
const request = session.queryAuditLogs('login', { username: ' ', result: '', from: null, to: null })
|
|
assert.equal(pending[0].url, '/api/v1/login-logs?page=1&limit=20')
|
|
pending[0].resolve(response({ items: [], total: 0, page: 1, limit: 20 }))
|
|
await request
|
|
await assert.rejects(session.queryAuditLogs('login', { action: 'create' }))
|
|
session.clear()
|
|
await assert.rejects(session.queryAuditLogs('login'), /请先登录/)
|
|
assert.equal(pending.length, 1)
|
|
})
|
|
|
|
test('late audit response cannot survive session change', async () => {
|
|
const { session, pending } = setup()
|
|
assert.equal(typeof session.queryAuditLogs, 'function')
|
|
const request = session.queryAuditLogs('login')
|
|
session.clear()
|
|
session.state.user = { id: 3, role: 'admin', username: 'fixture.otheradmin' }
|
|
pending[0].resolve(response({ items: [{ username: 'fixture.private' }], total: 1 }))
|
|
await assert.rejects(request, /会话已变化/)
|
|
})
|
|
|
|
test('route/filter changes discard out-of-order rows and invalidation clears loaded private data', async () => {
|
|
assert.equal(typeof audit.createAuditLogLoader, 'function')
|
|
const { session, pending } = setup()
|
|
const state = { items: [], total: 0, loading: false, error: '' }
|
|
const loader = audit.createAuditLogLoader(session, state)
|
|
const old = loader.load('login', {})
|
|
const current = loader.load('operation', {})
|
|
pending[1].resolve(response({ items: [{ id: 2 }], total: 1 }))
|
|
await current
|
|
pending[0].resolve(response({ items: [{ id: 1 }], total: 1 }))
|
|
await old
|
|
assert.deepEqual(state.items, [{ id: 2 }])
|
|
loader.invalidate()
|
|
assert.deepEqual(state.items, [])
|
|
assert.equal(state.total, 0)
|
|
const late = loader.load('login', {})
|
|
session.clear()
|
|
pending[2].resolve(response({ items: [{ id: 3 }], total: 1 }))
|
|
await late
|
|
assert.deepEqual(state.items, [])
|
|
assert.equal(state.error, '')
|
|
})
|
|
|
|
test('loading page two preserves total so pagination cannot clamp the page back to one', async () => {
|
|
const { session, pending } = setup()
|
|
const state = { items: [], total: 0, loading: false, error: '' }
|
|
const loader = audit.createAuditLogLoader(session, state)
|
|
const first = loader.load('login', { page: 1 })
|
|
pending[0].resolve(response({ items: [{ id: 50 }], total: 50 }))
|
|
await first
|
|
const second = loader.load('login', { page: 2 })
|
|
assert.equal(state.total, 50)
|
|
assert.equal(state.loading, true)
|
|
assert.deepEqual(state.items, [])
|
|
assert.equal(new URL(pending[1].url, 'https://fixture.invalid').searchParams.get('page'), '2')
|
|
pending[1].resolve(response({ items: [{ id: 30 }], total: 50 }))
|
|
await second
|
|
assert.deepEqual(state.items, [{ id: 30 }])
|
|
assert.equal(state.total, 50)
|
|
loader.invalidate()
|
|
assert.equal(state.total, 0)
|
|
assert.deepEqual(state.items, [])
|
|
})
|