2026-03-20 17:15:14 +08:00
|
|
|
process.env.JWT_SECRET = 'test-secret-key-for-jest-testing-minimum-32-chars-long';
|
|
|
|
|
process.env.NODE_ENV = 'test';
|
|
|
|
|
process.env.DB_DIALECT = 'sqlite';
|
|
|
|
|
process.env.DB_STORAGE = ':memory:';
|
|
|
|
|
|
|
|
|
|
const request = require('supertest');
|
|
|
|
|
const express = require('express');
|
|
|
|
|
const jwt = require('jsonwebtoken');
|
|
|
|
|
const { Op } = require('sequelize');
|
|
|
|
|
const { sequelize } = require('../db');
|
|
|
|
|
const OperationLog = require('../models/OperationLog');
|
|
|
|
|
|
|
|
|
|
const JWT_SECRET = 'test-secret-key-for-jest-testing-minimum-32-chars-long';
|
|
|
|
|
|
|
|
|
|
const createTestApp = () => {
|
|
|
|
|
const app = express();
|
|
|
|
|
app.use(express.json());
|
|
|
|
|
|
|
|
|
|
const authMiddleware = (req, res, next) => {
|
|
|
|
|
const token = req.headers.authorization?.replace('Bearer ', '');
|
|
|
|
|
if (!token) {
|
|
|
|
|
return res.status(401).json({ success: false, message: '未授权' });
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const decoded = jwt.verify(token, JWT_SECRET);
|
|
|
|
|
req.user = decoded;
|
|
|
|
|
next();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return res.status(401).json({ success: false, message: '无效的令牌' });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const operationLogsRouter = express.Router();
|
|
|
|
|
|
|
|
|
|
operationLogsRouter.get('/', authMiddleware, async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const {
|
|
|
|
|
page = 1,
|
|
|
|
|
pageSize = 20,
|
|
|
|
|
module,
|
|
|
|
|
operationType,
|
|
|
|
|
targetId,
|
|
|
|
|
operatorId,
|
|
|
|
|
keyword,
|
|
|
|
|
startDate,
|
|
|
|
|
endDate,
|
2026-03-27 19:12:16 +08:00
|
|
|
result,
|
2026-03-20 17:15:14 +08:00
|
|
|
} = req.query;
|
|
|
|
|
|
|
|
|
|
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
|
|
|
|
const limit = Math.min(parseInt(pageSize), 100);
|
|
|
|
|
|
|
|
|
|
const where = {};
|
|
|
|
|
|
|
|
|
|
if (module && module !== 'all') {
|
|
|
|
|
where.module = module;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (operationType && operationType !== 'all') {
|
|
|
|
|
where.operationType = operationType;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (targetId) {
|
|
|
|
|
where.targetId = { [Op.like]: `%${targetId}%` };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (operatorId) {
|
|
|
|
|
where.operatorId = operatorId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (keyword) {
|
|
|
|
|
where[Op.or] = [
|
|
|
|
|
{ operationDescription: { [Op.like]: `%${keyword}%` } },
|
|
|
|
|
{ targetName: { [Op.like]: `%${keyword}%` } },
|
2026-03-27 19:12:16 +08:00
|
|
|
{ operatorName: { [Op.like]: `%${keyword}%` } },
|
2026-03-20 17:15:14 +08:00
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (result && result !== 'all') {
|
|
|
|
|
where.result = result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (startDate || endDate) {
|
|
|
|
|
where.createdAt = {};
|
|
|
|
|
if (startDate) {
|
|
|
|
|
where.createdAt[Op.gte] = new Date(startDate);
|
|
|
|
|
}
|
|
|
|
|
if (endDate) {
|
|
|
|
|
const endOfDay = new Date(endDate);
|
|
|
|
|
endOfDay.setHours(23, 59, 59, 999);
|
|
|
|
|
where.createdAt[Op.lte] = endOfDay;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { count, rows: logs } = await OperationLog.findAndCountAll({
|
|
|
|
|
where,
|
|
|
|
|
order: [['createdAt', 'DESC']],
|
|
|
|
|
offset,
|
2026-03-27 19:12:16 +08:00
|
|
|
limit,
|
2026-03-20 17:15:14 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
data: {
|
|
|
|
|
total: count,
|
|
|
|
|
page: parseInt(page),
|
|
|
|
|
pageSize: parseInt(pageSize),
|
2026-03-27 19:12:16 +08:00
|
|
|
logs,
|
|
|
|
|
},
|
2026-03-20 17:15:14 +08:00
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取操作日志失败:', error);
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
success: false,
|
2026-03-27 19:12:16 +08:00
|
|
|
message: '获取操作日志失败',
|
2026-03-20 17:15:14 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
operationLogsRouter.get('/:recordId', authMiddleware, async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const log = await OperationLog.findByPk(req.params.recordId);
|
|
|
|
|
|
|
|
|
|
if (!log) {
|
|
|
|
|
return res.status(404).json({
|
|
|
|
|
success: false,
|
2026-03-27 19:12:16 +08:00
|
|
|
message: '日志记录不存在',
|
2026-03-20 17:15:14 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
success: true,
|
2026-03-27 19:12:16 +08:00
|
|
|
data: log,
|
2026-03-20 17:15:14 +08:00
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取操作日志详情失败:', error);
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
success: false,
|
2026-03-27 19:12:16 +08:00
|
|
|
message: '获取操作日志详情失败',
|
2026-03-20 17:15:14 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.use('/api/operation-logs', operationLogsRouter);
|
|
|
|
|
|
|
|
|
|
return app;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
describe('OperationLogs API 路由测试', () => {
|
|
|
|
|
let app;
|
|
|
|
|
let authToken;
|
|
|
|
|
const testUser = {
|
|
|
|
|
userId: 'test_user_001',
|
|
|
|
|
username: 'testuser',
|
|
|
|
|
realName: '测试用户',
|
2026-03-27 19:12:16 +08:00
|
|
|
roleName: '管理员',
|
2026-03-20 17:15:14 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
beforeAll(async () => {
|
|
|
|
|
await sequelize.sync({ force: true });
|
|
|
|
|
app = createTestApp();
|
|
|
|
|
authToken = jwt.sign(testUser, JWT_SECRET, { expiresIn: '24h' });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(async () => {
|
|
|
|
|
await OperationLog.destroy({ where: {} });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterAll(async () => {
|
|
|
|
|
await sequelize.close();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const createTestLog = async (data = {}) => {
|
|
|
|
|
const defaultData = {
|
|
|
|
|
recordId: `OPLOG_API_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
|
|
|
module: 'device',
|
|
|
|
|
operationType: 'create',
|
|
|
|
|
operationDescription: '测试日志',
|
|
|
|
|
targetId: 'DEV_TEST',
|
|
|
|
|
targetName: '测试设备',
|
|
|
|
|
operatorId: testUser.userId,
|
|
|
|
|
operatorName: testUser.realName,
|
|
|
|
|
operatorRole: testUser.roleName,
|
2026-03-27 19:12:16 +08:00
|
|
|
result: 'success',
|
2026-03-20 17:15:14 +08:00
|
|
|
};
|
|
|
|
|
return await OperationLog.create({ ...defaultData, ...data });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
describe('GET /api/operation-logs', () => {
|
|
|
|
|
test('未授权访问应该返回 401', async () => {
|
2026-03-27 19:12:16 +08:00
|
|
|
const response = await request(app).get('/api/operation-logs').expect(401);
|
2026-03-20 17:15:14 +08:00
|
|
|
|
|
|
|
|
expect(response.body.success).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('应该能够获取日志列表', async () => {
|
|
|
|
|
await createTestLog({ recordId: 'OPLOG_LST_001' });
|
|
|
|
|
await createTestLog({ recordId: 'OPLOG_LST_002' });
|
|
|
|
|
|
|
|
|
|
const response = await request(app)
|
|
|
|
|
.get('/api/operation-logs')
|
|
|
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
|
|
|
.expect(200);
|
|
|
|
|
|
|
|
|
|
expect(response.body.success).toBe(true);
|
|
|
|
|
expect(response.body.data.logs).toHaveLength(2);
|
|
|
|
|
expect(response.body.data.total).toBe(2);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('应该支持分页参数', async () => {
|
|
|
|
|
for (let i = 0; i < 15; i++) {
|
|
|
|
|
await createTestLog({ recordId: `OPLOG_PAGE_${i}` });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const response = await request(app)
|
|
|
|
|
.get('/api/operation-logs')
|
|
|
|
|
.query({ page: 1, pageSize: 10 })
|
|
|
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
|
|
|
.expect(200);
|
|
|
|
|
|
|
|
|
|
expect(response.body.success).toBe(true);
|
|
|
|
|
expect(response.body.data.logs).toHaveLength(10);
|
|
|
|
|
expect(response.body.data.total).toBe(15);
|
|
|
|
|
expect(response.body.data.page).toBe(1);
|
|
|
|
|
expect(response.body.data.pageSize).toBe(10);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('应该支持按 module 筛选', async () => {
|
|
|
|
|
await createTestLog({ module: 'device', recordId: 'OPLOG_MOD_1' });
|
|
|
|
|
await createTestLog({ module: 'user', recordId: 'OPLOG_MOD_2' });
|
|
|
|
|
await createTestLog({ module: 'role', recordId: 'OPLOG_MOD_3' });
|
|
|
|
|
|
|
|
|
|
const response = await request(app)
|
|
|
|
|
.get('/api/operation-logs')
|
|
|
|
|
.query({ module: 'device' })
|
|
|
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
|
|
|
.expect(200);
|
|
|
|
|
|
|
|
|
|
expect(response.body.data.logs).toHaveLength(1);
|
|
|
|
|
expect(response.body.data.logs[0].module).toBe('device');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('应该支持按 operationType 筛选', async () => {
|
|
|
|
|
await createTestLog({ operationType: 'create', recordId: 'OPLOG_TYPE_1' });
|
|
|
|
|
await createTestLog({ operationType: 'update', recordId: 'OPLOG_TYPE_2' });
|
|
|
|
|
await createTestLog({ operationType: 'delete', recordId: 'OPLOG_TYPE_3' });
|
|
|
|
|
|
|
|
|
|
const response = await request(app)
|
|
|
|
|
.get('/api/operation-logs')
|
|
|
|
|
.query({ operationType: 'create' })
|
|
|
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
|
|
|
.expect(200);
|
|
|
|
|
|
|
|
|
|
expect(response.body.data.logs).toHaveLength(1);
|
|
|
|
|
expect(response.body.data.logs[0].operationType).toBe('create');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('应该支持按 keyword 搜索', async () => {
|
2026-03-27 19:12:16 +08:00
|
|
|
await createTestLog({
|
|
|
|
|
operationDescription: '创建设备 SERVER_A',
|
|
|
|
|
recordId: 'OPLOG_KW_1',
|
|
|
|
|
targetName: '服务器A',
|
|
|
|
|
});
|
|
|
|
|
await createTestLog({
|
|
|
|
|
operationDescription: '更新设备 SERVER_B',
|
|
|
|
|
recordId: 'OPLOG_KW_2',
|
|
|
|
|
targetName: '服务器B',
|
|
|
|
|
});
|
|
|
|
|
await createTestLog({
|
|
|
|
|
operationDescription: '删除用户 USER_C',
|
|
|
|
|
recordId: 'OPLOG_KW_3',
|
|
|
|
|
targetName: '用户C',
|
|
|
|
|
});
|
2026-03-20 17:15:14 +08:00
|
|
|
|
|
|
|
|
const response = await request(app)
|
|
|
|
|
.get('/api/operation-logs')
|
|
|
|
|
.query({ keyword: 'SERVER' })
|
|
|
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
|
|
|
.expect(200);
|
|
|
|
|
|
|
|
|
|
expect(response.body.data.logs).toHaveLength(2);
|
2026-03-27 19:12:16 +08:00
|
|
|
expect(
|
|
|
|
|
response.body.data.logs.every(log => log.operationDescription.includes('SERVER'))
|
|
|
|
|
).toBe(true);
|
2026-03-20 17:15:14 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('应该支持按 result 筛选', async () => {
|
|
|
|
|
await createTestLog({ result: 'success', recordId: 'OPLOG_RES_1' });
|
|
|
|
|
await createTestLog({ result: 'failed', recordId: 'OPLOG_RES_2' });
|
|
|
|
|
|
|
|
|
|
const response = await request(app)
|
|
|
|
|
.get('/api/operation-logs')
|
|
|
|
|
.query({ result: 'failed' })
|
|
|
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
|
|
|
.expect(200);
|
|
|
|
|
|
|
|
|
|
expect(response.body.data.logs).toHaveLength(1);
|
|
|
|
|
expect(response.body.data.logs[0].result).toBe('failed');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('应该支持多条件组合筛选', async () => {
|
|
|
|
|
await createTestLog({
|
|
|
|
|
module: 'device',
|
|
|
|
|
operationType: 'create',
|
|
|
|
|
result: 'success',
|
2026-03-27 19:12:16 +08:00
|
|
|
recordId: 'OPLOG_COMB_1',
|
2026-03-20 17:15:14 +08:00
|
|
|
});
|
|
|
|
|
await createTestLog({
|
|
|
|
|
module: 'device',
|
|
|
|
|
operationType: 'update',
|
|
|
|
|
result: 'success',
|
2026-03-27 19:12:16 +08:00
|
|
|
recordId: 'OPLOG_COMB_2',
|
2026-03-20 17:15:14 +08:00
|
|
|
});
|
|
|
|
|
await createTestLog({
|
|
|
|
|
module: 'user',
|
|
|
|
|
operationType: 'create',
|
|
|
|
|
result: 'success',
|
2026-03-27 19:12:16 +08:00
|
|
|
recordId: 'OPLOG_COMB_3',
|
2026-03-20 17:15:14 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const response = await request(app)
|
|
|
|
|
.get('/api/operation-logs')
|
|
|
|
|
.query({ module: 'device', operationType: 'create' })
|
|
|
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
|
|
|
.expect(200);
|
|
|
|
|
|
|
|
|
|
expect(response.body.data.logs).toHaveLength(1);
|
|
|
|
|
expect(response.body.data.logs[0].module).toBe('device');
|
|
|
|
|
expect(response.body.data.logs[0].operationType).toBe('create');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('GET /api/operation-logs/:recordId', () => {
|
|
|
|
|
test('应该能够获取单个日志详情', async () => {
|
|
|
|
|
const log = await createTestLog({
|
|
|
|
|
recordId: 'OPLOG_DETAIL_001',
|
|
|
|
|
beforeState: { name: '旧名称' },
|
|
|
|
|
afterState: { name: '新名称' },
|
2026-03-27 19:12:16 +08:00
|
|
|
metadata: { customField: '自定义值' },
|
2026-03-20 17:15:14 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const response = await request(app)
|
|
|
|
|
.get('/api/operation-logs/OPLOG_DETAIL_001')
|
|
|
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
|
|
|
.expect(200);
|
|
|
|
|
|
|
|
|
|
expect(response.body.success).toBe(true);
|
|
|
|
|
expect(response.body.data.recordId).toBe('OPLOG_DETAIL_001');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('不存在的日志应该返回 404', async () => {
|
|
|
|
|
const response = await request(app)
|
|
|
|
|
.get('/api/operation-logs/NON_EXISTENT_LOG')
|
|
|
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
|
|
|
.expect(404);
|
|
|
|
|
|
|
|
|
|
expect(response.body.success).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|