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, []) })