feat: 添加操作日志、危险操作确认和业务关联功能
1. 新增操作日志记录功能,记录关键操作 2. 实现危险操作确认对话框,防止误删 3. 添加业务和库房管理模块 4. 支持设备标记为空闲状态 5. 完善API文档和健康检查 6. 优化前端删除操作的确认流程 7. 添加Swagger API文档支持 8. 实现设备与业务的关联功能 9. 改进设备模型,添加空闲相关字段 10. 优化用户、角色管理操作日志
This commit is contained in:
@@ -0,0 +1,545 @@
|
||||
const request = require('supertest');
|
||||
const express = require('express');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const bodyParser = require('body-parser');
|
||||
const { sequelize } = require('../db');
|
||||
const OperationLog = require('../models/OperationLog');
|
||||
const Device = require('../models/Device');
|
||||
const Rack = require('../models/Rack');
|
||||
const Room = require('../models/Room');
|
||||
const User = require('../models/User');
|
||||
const Role = require('../models/Role');
|
||||
const UserRole = require('../models/UserRole');
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
|
||||
|
||||
describe('设备/用户/角色操作日志集成测试', () => {
|
||||
let app;
|
||||
let authToken;
|
||||
let adminUser;
|
||||
let testRack;
|
||||
let testRoom;
|
||||
|
||||
beforeAll(async () => {
|
||||
await sequelize.sync({ force: true });
|
||||
|
||||
adminUser = await User.create({
|
||||
userId: 'admin_int_test',
|
||||
username: 'admin',
|
||||
password: '$2a$10$test',
|
||||
realName: '管理员',
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
testRoom = await Room.create({
|
||||
roomId: 'ROOM_INT_TEST',
|
||||
name: '测试机房',
|
||||
location: '测试位置',
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
testRack = await Rack.create({
|
||||
rackId: 'RACK_INT_TEST',
|
||||
name: '测试机柜',
|
||||
roomId: testRoom.roomId,
|
||||
totalPower: 10000,
|
||||
currentPower: 0,
|
||||
totalUnits: 48,
|
||||
usedUnits: 0,
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
app = createTestApp();
|
||||
authToken = jwt.sign(
|
||||
{ userId: adminUser.userId, username: adminUser.username, realName: adminUser.realName, roleName: '管理员' },
|
||||
JWT_SECRET,
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await OperationLog.destroy({ where: {} });
|
||||
await Device.destroy({ where: {} });
|
||||
});
|
||||
|
||||
const createTestApp = () => {
|
||||
const app = express();
|
||||
app.use(bodyParser.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 devicesRouter = require('../routes/devices');
|
||||
const usersRouter = require('../routes/users');
|
||||
const rolesRouter = require('../routes/roles');
|
||||
|
||||
app.use('/api/devices', authMiddleware, devicesRouter);
|
||||
app.use('/api/users', authMiddleware, usersRouter);
|
||||
app.use('/api/roles', authMiddleware, rolesRouter);
|
||||
|
||||
return app;
|
||||
};
|
||||
|
||||
describe('设备操作日志记录', () => {
|
||||
test('创建设备应该记录操作日志', async () => {
|
||||
const deviceData = {
|
||||
name: '集成测试设备',
|
||||
deviceId: `DEV_INT_${Date.now()}`,
|
||||
type: 'server',
|
||||
rackId: testRack.rackId,
|
||||
position: 1,
|
||||
height: 2,
|
||||
powerConsumption: 500,
|
||||
status: 'running'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/devices')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send(deviceData)
|
||||
.expect(201);
|
||||
|
||||
expect(response.body.name).toBe(deviceData.name);
|
||||
|
||||
const logs = await OperationLog.findAll({
|
||||
where: {
|
||||
module: 'device',
|
||||
operationType: 'create',
|
||||
targetId: response.body.deviceId
|
||||
}
|
||||
});
|
||||
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].operationDescription).toContain('创建设备');
|
||||
expect(logs[0].operatorId).toBe(adminUser.userId);
|
||||
expect(logs[0].result).toBe('success');
|
||||
});
|
||||
|
||||
test('更新设备应该记录操作日志', async () => {
|
||||
const device = await Device.create({
|
||||
deviceId: `DEV_UPD_INT_${Date.now()}`,
|
||||
name: '更新前设备',
|
||||
type: 'server',
|
||||
rackId: testRack.rackId,
|
||||
position: 5,
|
||||
height: 2,
|
||||
powerConsumption: 500,
|
||||
status: 'offline'
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.put(`/api/devices/${device.deviceId}`)
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({ name: '更新后设备', status: 'running' })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.name).toBe('更新后设备');
|
||||
|
||||
const logs = await OperationLog.findAll({
|
||||
where: {
|
||||
module: 'device',
|
||||
operationType: 'update',
|
||||
targetId: device.deviceId
|
||||
}
|
||||
});
|
||||
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].beforeState.name).toBe('更新前设备');
|
||||
expect(logs[0].afterState.name).toBe('更新后设备');
|
||||
});
|
||||
|
||||
test('删除设备应该记录操作日志', async () => {
|
||||
const device = await Device.create({
|
||||
deviceId: `DEV_DEL_INT_${Date.now()}`,
|
||||
name: '待删除设备',
|
||||
type: 'server',
|
||||
rackId: testRack.rackId,
|
||||
position: 10,
|
||||
height: 2,
|
||||
powerConsumption: 500,
|
||||
status: 'running'
|
||||
});
|
||||
|
||||
await request(app)
|
||||
.delete(`/api/devices/${device.deviceId}`)
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
const logs = await OperationLog.findAll({
|
||||
where: {
|
||||
module: 'device',
|
||||
operationType: 'delete',
|
||||
targetId: device.deviceId
|
||||
}
|
||||
});
|
||||
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].beforeState.name).toBe('待删除设备');
|
||||
});
|
||||
|
||||
test('批量删除设备应该记录操作日志', async () => {
|
||||
const device1 = await Device.create({
|
||||
deviceId: `DEV_BATCH1_${Date.now()}`,
|
||||
name: '批量设备1',
|
||||
type: 'server',
|
||||
rackId: testRack.rackId,
|
||||
position: 15,
|
||||
height: 2,
|
||||
powerConsumption: 500,
|
||||
status: 'running'
|
||||
});
|
||||
|
||||
const device2 = await Device.create({
|
||||
deviceId: `DEV_BATCH2_${Date.now()}`,
|
||||
name: '批量设备2',
|
||||
type: 'server',
|
||||
rackId: testRack.rackId,
|
||||
position: 20,
|
||||
height: 2,
|
||||
powerConsumption: 500,
|
||||
status: 'running'
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/devices/batch-delete')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({ deviceIds: [device1.deviceId, device2.deviceId] })
|
||||
.expect(200);
|
||||
|
||||
const logs = await OperationLog.findAll({
|
||||
where: { operationType: 'batch_delete' }
|
||||
});
|
||||
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].metadata.count).toBe(2);
|
||||
});
|
||||
|
||||
test('批量变更设备状态应该记录操作日志', async () => {
|
||||
const device1 = await Device.create({
|
||||
deviceId: `DEV_STAT1_${Date.now()}`,
|
||||
name: '状态设备1',
|
||||
type: 'server',
|
||||
rackId: testRack.rackId,
|
||||
position: 25,
|
||||
height: 2,
|
||||
powerConsumption: 500,
|
||||
status: 'offline'
|
||||
});
|
||||
|
||||
const device2 = await Device.create({
|
||||
deviceId: `DEV_STAT2_${Date.now()}`,
|
||||
name: '状态设备2',
|
||||
type: 'server',
|
||||
rackId: testRack.rackId,
|
||||
position: 30,
|
||||
height: 2,
|
||||
powerConsumption: 500,
|
||||
status: 'offline'
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.put('/api/devices/batch-status')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({ deviceIds: [device1.deviceId, device2.deviceId], status: 'running' })
|
||||
.expect(200);
|
||||
|
||||
const logs = await OperationLog.findAll({
|
||||
where: { operationType: 'status_change' }
|
||||
});
|
||||
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].metadata.status).toBe('running');
|
||||
expect(logs[0].metadata.count).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('用户操作日志记录', () => {
|
||||
let testRole;
|
||||
|
||||
beforeAll(async () => {
|
||||
testRole = await Role.create({
|
||||
roleId: `ROLE_INT_TEST_${Date.now()}`,
|
||||
roleName: '测试角色',
|
||||
roleCode: `test_role_${Date.now()}`,
|
||||
permissions: ['read', 'write'],
|
||||
status: 'active'
|
||||
});
|
||||
});
|
||||
|
||||
test('创建用户应该记录操作日志', async () => {
|
||||
const userData = {
|
||||
username: `int_test_user_${Date.now()}`,
|
||||
password: 'Password123!',
|
||||
realName: '集成测试用户',
|
||||
email: `test_${Date.now()}@example.com`,
|
||||
roleIds: [testRole.roleId]
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/users')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send(userData)
|
||||
.expect(201);
|
||||
|
||||
const logs = await OperationLog.findAll({
|
||||
where: {
|
||||
module: 'user',
|
||||
operationType: 'create',
|
||||
targetName: userData.username
|
||||
}
|
||||
});
|
||||
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].operationDescription).toContain(`创建用户 ${userData.username}`);
|
||||
expect(logs[0].metadata.roleIds).toContain(testRole.roleId);
|
||||
|
||||
await User.destroy({ where: { userId: response.body.userId } });
|
||||
});
|
||||
|
||||
test('更新用户应该记录操作日志', async () => {
|
||||
const user = await User.create({
|
||||
userId: `USER_UPD_INT_${Date.now()}`,
|
||||
username: `old_name_${Date.now()}`,
|
||||
password: 'Password123!',
|
||||
realName: '旧名称用户',
|
||||
email: `old_${Date.now()}@example.com`,
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.put(`/api/users/${user.userId}`)
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({ realName: '新名称用户' })
|
||||
.expect(200);
|
||||
|
||||
const logs = await OperationLog.findAll({
|
||||
where: {
|
||||
module: 'user',
|
||||
operationType: 'update',
|
||||
targetId: user.userId
|
||||
}
|
||||
});
|
||||
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].beforeState.realName).toBe('旧名称用户');
|
||||
expect(logs[0].afterState.realName).toBe('新名称用户');
|
||||
|
||||
await User.destroy({ where: { userId: user.userId } });
|
||||
});
|
||||
|
||||
test('变更用户角色应该记录权限变更日志', async () => {
|
||||
const user = await User.create({
|
||||
userId: `USER_ROLE_INT_${Date.now()}`,
|
||||
username: `role_test_${Date.now()}`,
|
||||
password: 'Password123!',
|
||||
realName: '角色测试用户',
|
||||
email: `role_${Date.now()}@example.com`,
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
const newRole = await Role.create({
|
||||
roleId: `ROLE_NEW_INT_${Date.now()}`,
|
||||
roleName: '新测试角色',
|
||||
roleCode: `new_role_${Date.now()}`,
|
||||
permissions: ['admin'],
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
await UserRole.create({
|
||||
UserId: user.userId,
|
||||
RoleId: testRole.roleId
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.put(`/api/users/${user.userId}`)
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({ roleIds: [newRole.roleId] })
|
||||
.expect(200);
|
||||
|
||||
const logs = await OperationLog.findAll({
|
||||
where: {
|
||||
module: 'user',
|
||||
operationType: 'permission_change',
|
||||
targetId: user.userId
|
||||
}
|
||||
});
|
||||
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].metadata.oldRoleIds).toContain(testRole.roleId);
|
||||
expect(logs[0].metadata.newRoleIds).toContain(newRole.roleId);
|
||||
|
||||
await UserRole.destroy({ where: { UserId: user.userId } });
|
||||
await User.destroy({ where: { userId: user.userId } });
|
||||
await Role.destroy({ where: { roleId: newRole.roleId } });
|
||||
});
|
||||
|
||||
test('删除用户应该记录操作日志', async () => {
|
||||
const user = await User.create({
|
||||
userId: `USER_DEL_INT_${Date.now()}`,
|
||||
username: `del_test_${Date.now()}`,
|
||||
password: 'Password123!',
|
||||
realName: '删除测试用户',
|
||||
email: `del_${Date.now()}@example.com`,
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
await request(app)
|
||||
.delete(`/api/users/${user.userId}`)
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
const logs = await OperationLog.findAll({
|
||||
where: {
|
||||
module: 'user',
|
||||
operationType: 'delete',
|
||||
targetId: user.userId
|
||||
}
|
||||
});
|
||||
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].beforeState.username).toBe(user.username);
|
||||
});
|
||||
});
|
||||
|
||||
describe('角色操作日志记录', () => {
|
||||
test('创建角色应该记录操作日志', async () => {
|
||||
const roleData = {
|
||||
roleName: `集成测试角色_${Date.now()}`,
|
||||
roleCode: `int_test_role_${Date.now()}`,
|
||||
description: '集成测试用角色',
|
||||
permissions: ['read', 'write'],
|
||||
status: 'active'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/roles')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send(roleData)
|
||||
.expect(201);
|
||||
|
||||
const logs = await OperationLog.findAll({
|
||||
where: {
|
||||
module: 'role',
|
||||
operationType: 'create',
|
||||
targetId: response.body.roleId
|
||||
}
|
||||
});
|
||||
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].operationDescription).toContain(`创建角色 ${roleData.roleName}`);
|
||||
expect(logs[0].metadata.roleCode).toBe(roleData.roleCode);
|
||||
|
||||
await Role.destroy({ where: { roleId: response.body.roleId } });
|
||||
});
|
||||
|
||||
test('更新角色应该记录操作日志', async () => {
|
||||
const role = await Role.create({
|
||||
roleId: `ROLE_UPD_INT_${Date.now()}`,
|
||||
roleName: `旧角色名_${Date.now()}`,
|
||||
roleCode: `old_role_${Date.now()}`,
|
||||
description: '旧描述',
|
||||
permissions: ['read'],
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.put(`/api/roles/${role.roleId}`)
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({
|
||||
roleName: `新角色名_${Date.now()}`,
|
||||
permissions: ['read', 'write', 'delete']
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const logs = await OperationLog.findAll({
|
||||
where: {
|
||||
module: 'role',
|
||||
operationType: 'update',
|
||||
targetId: role.roleId
|
||||
}
|
||||
});
|
||||
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].beforeState.roleName).toContain('旧角色名');
|
||||
expect(logs[0].afterState.roleName).toContain('新角色名');
|
||||
|
||||
await Role.destroy({ where: { roleId: role.roleId } });
|
||||
});
|
||||
|
||||
test('删除角色应该记录操作日志', async () => {
|
||||
const role = await Role.create({
|
||||
roleId: `ROLE_DEL_INT_${Date.now()}`,
|
||||
roleName: `待删除角色_${Date.now()}`,
|
||||
roleCode: `del_role_${Date.now()}`,
|
||||
description: '待删除',
|
||||
permissions: ['read'],
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
await request(app)
|
||||
.delete(`/api/roles/${role.roleId}`)
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
const logs = await OperationLog.findAll({
|
||||
where: {
|
||||
module: 'role',
|
||||
operationType: 'delete',
|
||||
targetId: role.roleId
|
||||
}
|
||||
});
|
||||
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].beforeState.roleName).toContain('待删除角色');
|
||||
});
|
||||
});
|
||||
|
||||
describe('日志查询验证', () => {
|
||||
test('通过 API 应该能够查询到刚记录的操作日志', async () => {
|
||||
const device = await Device.create({
|
||||
deviceId: `DEV_QUERY_INT_${Date.now()}`,
|
||||
name: '查询测试设备',
|
||||
type: 'server',
|
||||
rackId: testRack.rackId,
|
||||
position: 40,
|
||||
height: 2,
|
||||
powerConsumption: 500,
|
||||
status: 'running'
|
||||
});
|
||||
|
||||
await request(app)
|
||||
.put(`/api/devices/${device.deviceId}`)
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({ status: 'maintenance' })
|
||||
.expect(200);
|
||||
|
||||
const logsResponse = await request(app)
|
||||
.get('/api/devices')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
const logs = await OperationLog.findAll({
|
||||
where: {
|
||||
module: 'device',
|
||||
operationType: 'update',
|
||||
targetId: device.deviceId
|
||||
}
|
||||
});
|
||||
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].beforeState.status).toBe('running');
|
||||
expect(logs[0].afterState.status).toBe('maintenance');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,277 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../db');
|
||||
const OperationLog = require('../models/OperationLog');
|
||||
|
||||
describe('OperationLog 模型测试', () => {
|
||||
beforeAll(async () => {
|
||||
await sequelize.sync({ force: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await OperationLog.destroy({ where: {} });
|
||||
});
|
||||
|
||||
describe('模型定义', () => {
|
||||
test('OperationLog 模型应该正确定义', () => {
|
||||
expect(OperationLog).toBeDefined();
|
||||
expect(typeof OperationLog.create).toBe('function');
|
||||
expect(typeof OperationLog.findAll).toBe('function');
|
||||
expect(typeof OperationLog.findOne).toBe('function');
|
||||
});
|
||||
|
||||
test('模型应该有正确的表名', () => {
|
||||
expect(OperationLog.options.tableName).toBe('operation_logs');
|
||||
});
|
||||
});
|
||||
|
||||
describe('创建日志记录', () => {
|
||||
test('应该能够创建基本的操作日志', async () => {
|
||||
const logData = {
|
||||
recordId: 'OPLOG_TEST_001',
|
||||
module: 'device',
|
||||
operationType: 'create',
|
||||
operationDescription: '创建设备 TEST_SERVER',
|
||||
targetId: 'DEV001',
|
||||
targetName: 'TEST_SERVER',
|
||||
operatorId: 'user_001',
|
||||
operatorName: '测试用户',
|
||||
operatorRole: '管理员',
|
||||
result: 'success'
|
||||
};
|
||||
|
||||
const log = await OperationLog.create(logData);
|
||||
|
||||
expect(log.recordId).toBe(logData.recordId);
|
||||
expect(log.module).toBe(logData.module);
|
||||
expect(log.operationType).toBe(logData.operationType);
|
||||
expect(log.operationDescription).toBe(logData.operationDescription);
|
||||
expect(log.targetId).toBe(logData.targetId);
|
||||
expect(log.targetName).toBe(logData.targetName);
|
||||
expect(log.operatorId).toBe(logData.operatorId);
|
||||
expect(log.operatorName).toBe(logData.operatorName);
|
||||
expect(log.result).toBe(logData.result);
|
||||
expect(log.createdAt).toBeDefined();
|
||||
expect(log.updatedAt).toBeDefined();
|
||||
});
|
||||
|
||||
test('应该能够创建带有 beforeState 和 afterState 的日志', async () => {
|
||||
const beforeState = { name: '旧名称', status: 'offline' };
|
||||
const afterState = { name: '新名称', status: 'running' };
|
||||
|
||||
const log = await OperationLog.create({
|
||||
recordId: 'OPLOG_TEST_002',
|
||||
module: 'device',
|
||||
operationType: 'update',
|
||||
operationDescription: '更新设备 TEST_DEVICE',
|
||||
targetId: 'DEV002',
|
||||
targetName: 'TEST_DEVICE',
|
||||
operatorId: 'user_001',
|
||||
operatorName: '测试用户',
|
||||
beforeState,
|
||||
afterState,
|
||||
result: 'success'
|
||||
});
|
||||
|
||||
expect(log.beforeState).toEqual(beforeState);
|
||||
expect(log.afterState).toEqual(afterState);
|
||||
});
|
||||
|
||||
test('应该能够创建带有 metadata 的日志', async () => {
|
||||
const metadata = {
|
||||
count: 5,
|
||||
source: 'batch_operation',
|
||||
extraInfo: '额外信息'
|
||||
};
|
||||
|
||||
const log = await OperationLog.create({
|
||||
recordId: 'OPLOG_TEST_003',
|
||||
module: 'device',
|
||||
operationType: 'batch_delete',
|
||||
operationDescription: '批量删除设备',
|
||||
targetId: 'DEV001,DEV002,DEV003',
|
||||
targetName: '3台设备',
|
||||
operatorId: 'user_001',
|
||||
operatorName: '测试用户',
|
||||
metadata,
|
||||
result: 'success'
|
||||
});
|
||||
|
||||
expect(log.metadata).toEqual(metadata);
|
||||
});
|
||||
|
||||
test('应该能够创建带有 IP 地址和 UserAgent 的日志', async () => {
|
||||
const log = await OperationLog.create({
|
||||
recordId: 'OPLOG_TEST_004',
|
||||
module: 'user',
|
||||
operationType: 'create',
|
||||
operationDescription: '创建用户 test_user',
|
||||
targetId: 'user_test',
|
||||
targetName: 'test_user',
|
||||
operatorId: 'admin_user',
|
||||
operatorName: '管理员',
|
||||
ipAddress: '192.168.1.100',
|
||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
|
||||
result: 'success'
|
||||
});
|
||||
|
||||
expect(log.ipAddress).toBe('192.168.1.100');
|
||||
expect(log.userAgent).toBe('Mozilla/5.0 (Windows NT 10.0; Win64; x64)');
|
||||
});
|
||||
|
||||
test('result 字段默认为 success', async () => {
|
||||
const log = await OperationLog.create({
|
||||
recordId: 'OPLOG_TEST_005',
|
||||
module: 'device',
|
||||
operationType: 'delete',
|
||||
operationDescription: '删除设备',
|
||||
targetId: 'DEV005',
|
||||
targetName: 'TEST_DEVICE',
|
||||
operatorId: 'user_001',
|
||||
operatorName: '测试用户'
|
||||
});
|
||||
|
||||
expect(log.result).toBe('success');
|
||||
});
|
||||
|
||||
test('metadata 字段默认为空对象', async () => {
|
||||
const log = await OperationLog.create({
|
||||
recordId: 'OPLOG_TEST_006',
|
||||
module: 'role',
|
||||
operationType: 'create',
|
||||
operationDescription: '创建角色',
|
||||
targetId: 'role_test',
|
||||
targetName: '测试角色',
|
||||
operatorId: 'user_001',
|
||||
operatorName: '测试用户'
|
||||
});
|
||||
|
||||
expect(log.metadata).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('查询日志记录', () => {
|
||||
beforeEach(async () => {
|
||||
await OperationLog.bulkCreate([
|
||||
{
|
||||
recordId: 'OPLOG_QUERY_001',
|
||||
module: 'device',
|
||||
operationType: 'create',
|
||||
operationDescription: '创建设备 设备A',
|
||||
targetId: 'DEV_A',
|
||||
targetName: '设备A',
|
||||
operatorId: 'user_001',
|
||||
operatorName: '用户A',
|
||||
result: 'success'
|
||||
},
|
||||
{
|
||||
recordId: 'OPLOG_QUERY_002',
|
||||
module: 'device',
|
||||
operationType: 'update',
|
||||
operationDescription: '更新设备 设备B',
|
||||
targetId: 'DEV_B',
|
||||
targetName: '设备B',
|
||||
operatorId: 'user_002',
|
||||
operatorName: '用户B',
|
||||
result: 'success'
|
||||
},
|
||||
{
|
||||
recordId: 'OPLOG_QUERY_003',
|
||||
module: 'user',
|
||||
operationType: 'create',
|
||||
operationDescription: '创建用户 用户C',
|
||||
targetId: 'user_C',
|
||||
targetName: '用户C',
|
||||
operatorId: 'user_001',
|
||||
operatorName: '用户A',
|
||||
result: 'success'
|
||||
},
|
||||
{
|
||||
recordId: 'OPLOG_QUERY_004',
|
||||
module: 'device',
|
||||
operationType: 'delete',
|
||||
operationDescription: '删除设备 设备D',
|
||||
targetId: 'DEV_D',
|
||||
targetName: '设备D',
|
||||
operatorId: 'user_001',
|
||||
operatorName: '用户A',
|
||||
result: 'failed'
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
test('应该能够按 module 查询', async () => {
|
||||
const deviceLogs = await OperationLog.findAll({
|
||||
where: { module: 'device' }
|
||||
});
|
||||
expect(deviceLogs.length).toBe(3);
|
||||
});
|
||||
|
||||
test('应该能够按 operationType 查询', async () => {
|
||||
const createLogs = await OperationLog.findAll({
|
||||
where: { operationType: 'create' }
|
||||
});
|
||||
expect(createLogs.length).toBe(2);
|
||||
});
|
||||
|
||||
test('应该能够按 operatorId 查询', async () => {
|
||||
const user001Logs = await OperationLog.findAll({
|
||||
where: { operatorId: 'user_001' }
|
||||
});
|
||||
expect(user001Logs.length).toBe(3);
|
||||
});
|
||||
|
||||
test('应该能够按 result 查询', async () => {
|
||||
const failedLogs = await OperationLog.findAll({
|
||||
where: { result: 'failed' }
|
||||
});
|
||||
expect(failedLogs.length).toBe(1);
|
||||
});
|
||||
|
||||
test('应该能够按 targetId 模糊查询', async () => {
|
||||
const { Op } = require('sequelize');
|
||||
const logs = await OperationLog.findAll({
|
||||
where: {
|
||||
targetId: { [Op.like]: '%DEV%' }
|
||||
}
|
||||
});
|
||||
expect(logs.length).toBe(3);
|
||||
});
|
||||
|
||||
test('应该支持分页查询', async () => {
|
||||
const { count, rows } = await OperationLog.findAndCountAll({
|
||||
limit: 2,
|
||||
offset: 0,
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
expect(count).toBe(4);
|
||||
expect(rows.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('索引测试', () => {
|
||||
test('应该有 module 索引', () => {
|
||||
const indexes = OperationLog.options.indexes;
|
||||
expect(indexes.some(idx => idx.fields.includes('module'))).toBe(true);
|
||||
});
|
||||
|
||||
test('应该有 operationType 索引', () => {
|
||||
const indexes = OperationLog.options.indexes;
|
||||
expect(indexes.some(idx => idx.fields.includes('operationType'))).toBe(true);
|
||||
});
|
||||
|
||||
test('应该有 targetId 索引', () => {
|
||||
const indexes = OperationLog.options.indexes;
|
||||
expect(indexes.some(idx => idx.fields.includes('targetId'))).toBe(true);
|
||||
});
|
||||
|
||||
test('应该有 operatorId 索引', () => {
|
||||
const indexes = OperationLog.options.indexes;
|
||||
expect(indexes.some(idx => idx.fields.includes('operatorId'))).toBe(true);
|
||||
});
|
||||
|
||||
test('应该有 createdAt 索引', () => {
|
||||
const indexes = OperationLog.options.indexes;
|
||||
expect(indexes.some(idx => idx.fields.includes('createdAt'))).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,477 @@
|
||||
const { sequelize } = require('../db');
|
||||
const OperationLog = require('../models/OperationLog');
|
||||
const {
|
||||
logOperation,
|
||||
logDeviceOperation,
|
||||
logUserOperation,
|
||||
logRoleOperation
|
||||
} = require('../utils/operationLogger');
|
||||
|
||||
describe('operationLogger 工具函数测试', () => {
|
||||
beforeAll(async () => {
|
||||
await sequelize.sync({ force: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await OperationLog.destroy({ where: {} });
|
||||
});
|
||||
|
||||
describe('logOperation 基础函数', () => {
|
||||
test('应该能够记录基本操作日志', async () => {
|
||||
const mockReq = {
|
||||
user: {
|
||||
userId: 'user_test_001',
|
||||
realName: '测试用户',
|
||||
roleName: '管理员'
|
||||
},
|
||||
headers: {
|
||||
'x-forwarded-for': '192.168.1.100',
|
||||
'user-agent': 'Mozilla/5.0 Test Browser'
|
||||
}
|
||||
};
|
||||
|
||||
await logOperation({
|
||||
module: 'device',
|
||||
operationType: 'create',
|
||||
operationDescription: '测试创建设备',
|
||||
targetId: 'DEV_TEST_001',
|
||||
targetName: '测试设备',
|
||||
beforeState: null,
|
||||
afterState: { name: '测试设备', status: 'running' },
|
||||
result: 'success',
|
||||
req: mockReq
|
||||
});
|
||||
|
||||
const logs = await OperationLog.findAll({
|
||||
where: { targetId: 'DEV_TEST_001' }
|
||||
});
|
||||
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].module).toBe('device');
|
||||
expect(logs[0].operationType).toBe('create');
|
||||
expect(logs[0].operationDescription).toBe('测试创建设备');
|
||||
expect(logs[0].targetId).toBe('DEV_TEST_001');
|
||||
expect(logs[0].targetName).toBe('测试设备');
|
||||
expect(logs[0].operatorId).toBe('user_test_001');
|
||||
expect(logs[0].operatorName).toBe('测试用户');
|
||||
expect(logs[0].operatorRole).toBe('管理员');
|
||||
expect(logs[0].ipAddress).toBe('192.168.1.100');
|
||||
expect(logs[0].userAgent).toBe('Mozilla/5.0 Test Browser');
|
||||
expect(logs[0].result).toBe('success');
|
||||
});
|
||||
|
||||
test('应该能够处理没有用户信息的请求', async () => {
|
||||
const mockReq = {
|
||||
user: null,
|
||||
headers: {}
|
||||
};
|
||||
|
||||
await logOperation({
|
||||
module: 'system',
|
||||
operationType: 'batch_update',
|
||||
operationDescription: '系统批量更新',
|
||||
targetId: 'SYSTEM',
|
||||
targetName: '系统',
|
||||
result: 'success',
|
||||
req: mockReq
|
||||
});
|
||||
|
||||
const logs = await OperationLog.findAll({
|
||||
where: { targetId: 'SYSTEM' }
|
||||
});
|
||||
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].operatorId).toBe('system');
|
||||
expect(logs[0].operatorName).toBe('系统');
|
||||
});
|
||||
|
||||
test('应该能够处理没有请求对象的场景', async () => {
|
||||
await logOperation({
|
||||
module: 'device',
|
||||
operationType: 'update',
|
||||
operationDescription: '无请求更新',
|
||||
targetId: 'DEV_NO_REQ',
|
||||
targetName: '无请求设备',
|
||||
result: 'success',
|
||||
req: null
|
||||
});
|
||||
|
||||
const logs = await OperationLog.findAll({
|
||||
where: { targetId: 'DEV_NO_REQ' }
|
||||
});
|
||||
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].ipAddress).toBeNull();
|
||||
expect(logs[0].userAgent).toBeNull();
|
||||
});
|
||||
|
||||
test('应该能够记录失败的操作', async () => {
|
||||
const mockReq = {
|
||||
user: { userId: 'user_fail', realName: '失败用户' },
|
||||
headers: {}
|
||||
};
|
||||
|
||||
await logOperation({
|
||||
module: 'device',
|
||||
operationType: 'delete',
|
||||
operationDescription: '删除设备失败',
|
||||
targetId: 'DEV_FAIL',
|
||||
targetName: '失败设备',
|
||||
result: 'failed',
|
||||
req: mockReq,
|
||||
metadata: { errorMessage: '设备不存在' }
|
||||
});
|
||||
|
||||
const logs = await OperationLog.findAll({
|
||||
where: { targetId: 'DEV_FAIL' }
|
||||
});
|
||||
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].result).toBe('failed');
|
||||
});
|
||||
|
||||
test('应该使用提供的 metadata', async () => {
|
||||
const mockReq = {
|
||||
user: { userId: 'user_meta', realName: '元数据用户' },
|
||||
headers: {}
|
||||
};
|
||||
|
||||
const customMetadata = {
|
||||
batchCount: 10,
|
||||
source: 'import',
|
||||
duration: 5000
|
||||
};
|
||||
|
||||
await logOperation({
|
||||
module: 'device',
|
||||
operationType: 'batch_update',
|
||||
operationDescription: '批量更新设备',
|
||||
targetId: 'DEV_BATCH',
|
||||
targetName: '批量设备',
|
||||
result: 'success',
|
||||
req: mockReq,
|
||||
metadata: customMetadata
|
||||
});
|
||||
|
||||
const logs = await OperationLog.findAll({
|
||||
where: { targetId: 'DEV_BATCH' }
|
||||
});
|
||||
|
||||
expect(logs[0].metadata).toEqual(customMetadata);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logDeviceOperation 设备操作日志', () => {
|
||||
test('应该记录设备创建日志', async () => {
|
||||
const mockReq = {
|
||||
user: { userId: 'user_dev', realName: '设备管理员' },
|
||||
headers: { 'x-forwarded-for': '10.0.0.1' }
|
||||
};
|
||||
|
||||
await logDeviceOperation(
|
||||
'create',
|
||||
'创建设备 测试服务器 (DEV001)',
|
||||
{
|
||||
targetId: 'DEV001',
|
||||
targetName: '测试服务器',
|
||||
afterState: { deviceId: 'DEV001', name: '测试服务器', status: 'running' },
|
||||
req: mockReq
|
||||
}
|
||||
);
|
||||
|
||||
const logs = await OperationLog.findAll({
|
||||
where: { module: 'device', operationType: 'create' }
|
||||
});
|
||||
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].module).toBe('device');
|
||||
expect(logs[0].operationType).toBe('create');
|
||||
expect(logs[0].targetId).toBe('DEV001');
|
||||
expect(logs[0].targetName).toBe('测试服务器');
|
||||
});
|
||||
|
||||
test('应该记录设备更新日志并包含状态变更', async () => {
|
||||
const mockReq = {
|
||||
user: { userId: 'user_upd', realName: '更新操作员' },
|
||||
headers: {}
|
||||
};
|
||||
|
||||
const beforeState = { name: '旧名称', status: 'offline' };
|
||||
const afterState = { name: '新名称', status: 'running' };
|
||||
|
||||
await logDeviceOperation(
|
||||
'update',
|
||||
'更新设备 DEV002',
|
||||
{
|
||||
targetId: 'DEV002',
|
||||
targetName: 'DEV002',
|
||||
beforeState,
|
||||
afterState,
|
||||
req: mockReq
|
||||
}
|
||||
);
|
||||
|
||||
const logs = await OperationLog.findAll({
|
||||
where: { targetId: 'DEV002', operationType: 'update' }
|
||||
});
|
||||
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].beforeState).toEqual(beforeState);
|
||||
expect(logs[0].afterState).toEqual(afterState);
|
||||
});
|
||||
|
||||
test('应该记录设备删除日志', async () => {
|
||||
const mockReq = {
|
||||
user: { userId: 'user_del', realName: '删除操作员' },
|
||||
headers: {}
|
||||
};
|
||||
|
||||
await logDeviceOperation(
|
||||
'delete',
|
||||
'删除设备 DEV003 (测试服务器)',
|
||||
{
|
||||
targetId: 'DEV003',
|
||||
targetName: '测试服务器',
|
||||
beforeState: { deviceId: 'DEV003', name: '测试服务器' },
|
||||
req: mockReq
|
||||
}
|
||||
);
|
||||
|
||||
const logs = await OperationLog.findAll({
|
||||
where: { targetId: 'DEV003', operationType: 'delete' }
|
||||
});
|
||||
|
||||
expect(logs.length).toBe(1);
|
||||
});
|
||||
|
||||
test('应该记录批量删除日志', async () => {
|
||||
const mockReq = {
|
||||
user: { userId: 'user_batch', realName: '批量操作员' },
|
||||
headers: {}
|
||||
};
|
||||
|
||||
await logDeviceOperation(
|
||||
'batch_delete',
|
||||
'批量删除设备 3台 (DEV_A,DEV_B,DEV_C)',
|
||||
{
|
||||
targetId: 'DEV_A,DEV_B,DEV_C',
|
||||
targetName: '3台设备',
|
||||
beforeState: [
|
||||
{ deviceId: 'DEV_A', name: '设备A' },
|
||||
{ deviceId: 'DEV_B', name: '设备B' },
|
||||
{ deviceId: 'DEV_C', name: '设备C' }
|
||||
],
|
||||
req: mockReq,
|
||||
metadata: { count: 3 }
|
||||
}
|
||||
);
|
||||
|
||||
const logs = await OperationLog.findAll({
|
||||
where: { operationType: 'batch_delete' }
|
||||
});
|
||||
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].metadata.count).toBe(3);
|
||||
});
|
||||
|
||||
test('应该记录状态变更日志', async () => {
|
||||
const mockReq = {
|
||||
user: { userId: 'user_status', realName: '状态管理员' },
|
||||
headers: {}
|
||||
};
|
||||
|
||||
await logDeviceOperation(
|
||||
'status_change',
|
||||
'批量变更设备状态为"运行中"',
|
||||
{
|
||||
targetId: 'DEV_STATUS_1,DEV_STATUS_2',
|
||||
targetName: '2台设备',
|
||||
beforeState: [
|
||||
{ deviceId: 'DEV_STATUS_1', status: 'offline' },
|
||||
{ deviceId: 'DEV_STATUS_2', status: 'maintenance' }
|
||||
],
|
||||
afterState: [
|
||||
{ deviceId: 'DEV_STATUS_1', status: 'running' },
|
||||
{ deviceId: 'DEV_STATUS_2', status: 'running' }
|
||||
],
|
||||
req: mockReq,
|
||||
metadata: { status: 'running', count: 2 }
|
||||
}
|
||||
);
|
||||
|
||||
const logs = await OperationLog.findAll({
|
||||
where: { operationType: 'status_change' }
|
||||
});
|
||||
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].metadata.status).toBe('running');
|
||||
});
|
||||
});
|
||||
|
||||
describe('logUserOperation 用户操作日志', () => {
|
||||
test('应该记录用户创建日志', async () => {
|
||||
const mockReq = {
|
||||
user: { userId: 'admin', realName: '系统管理员' },
|
||||
headers: {}
|
||||
};
|
||||
|
||||
await logUserOperation(
|
||||
'create',
|
||||
'创建用户 new_user',
|
||||
{
|
||||
targetId: 'user_new',
|
||||
targetName: 'new_user',
|
||||
afterState: { username: 'new_user', email: 'new@example.com' },
|
||||
req: mockReq,
|
||||
metadata: { roleIds: ['role_admin'] }
|
||||
}
|
||||
);
|
||||
|
||||
const logs = await OperationLog.findAll({
|
||||
where: { module: 'user', operationType: 'create' }
|
||||
});
|
||||
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].module).toBe('user');
|
||||
expect(logs[0].targetName).toBe('new_user');
|
||||
});
|
||||
|
||||
test('应该记录权限变更日志', async () => {
|
||||
const mockReq = {
|
||||
user: { userId: 'admin', realName: '管理员' },
|
||||
headers: {}
|
||||
};
|
||||
|
||||
await logUserOperation(
|
||||
'permission_change',
|
||||
'变更用户 test_user 的角色',
|
||||
{
|
||||
targetId: 'user_test',
|
||||
targetName: 'test_user',
|
||||
beforeState: { roleIds: ['role_viewer'] },
|
||||
afterState: { roleIds: ['role_admin'] },
|
||||
req: mockReq,
|
||||
metadata: { oldRoleIds: ['role_viewer'], newRoleIds: ['role_admin'] }
|
||||
}
|
||||
);
|
||||
|
||||
const logs = await OperationLog.findAll({
|
||||
where: { operationType: 'permission_change' }
|
||||
});
|
||||
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].metadata.oldRoleIds).toEqual(['role_viewer']);
|
||||
expect(logs[0].metadata.newRoleIds).toEqual(['role_admin']);
|
||||
});
|
||||
|
||||
test('应该记录用户删除日志', async () => {
|
||||
const mockReq = {
|
||||
user: { userId: 'admin', realName: '管理员' },
|
||||
headers: {}
|
||||
};
|
||||
|
||||
await logUserOperation(
|
||||
'delete',
|
||||
'删除用户 deleted_user',
|
||||
{
|
||||
targetId: 'user_deleted',
|
||||
targetName: 'deleted_user',
|
||||
beforeState: { username: 'deleted_user', email: 'deleted@example.com' },
|
||||
req: mockReq
|
||||
}
|
||||
);
|
||||
|
||||
const logs = await OperationLog.findAll({
|
||||
where: { module: 'user', operationType: 'delete' }
|
||||
});
|
||||
|
||||
expect(logs.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logRoleOperation 角色操作日志', () => {
|
||||
test('应该记录角色创建日志', async () => {
|
||||
const mockReq = {
|
||||
user: { userId: 'admin', realName: '管理员' },
|
||||
headers: {}
|
||||
};
|
||||
|
||||
await logRoleOperation(
|
||||
'create',
|
||||
'创建角色 测试角色',
|
||||
{
|
||||
targetId: 'role_test',
|
||||
targetName: '测试角色',
|
||||
afterState: { roleName: '测试角色', permissions: ['read', 'write'] },
|
||||
req: mockReq,
|
||||
metadata: { roleCode: 'test_role', permissions: ['read', 'write'] }
|
||||
}
|
||||
);
|
||||
|
||||
const logs = await OperationLog.findAll({
|
||||
where: { module: 'role', operationType: 'create' }
|
||||
});
|
||||
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].targetName).toBe('测试角色');
|
||||
expect(logs[0].metadata.roleCode).toBe('test_role');
|
||||
});
|
||||
|
||||
test('应该记录角色更新日志', async () => {
|
||||
const mockReq = {
|
||||
user: { userId: 'admin', realName: '管理员' },
|
||||
headers: {}
|
||||
};
|
||||
|
||||
const beforeState = { roleName: '旧角色', permissions: ['read'] };
|
||||
const afterState = { roleName: '新角色', permissions: ['read', 'write', 'delete'] };
|
||||
|
||||
await logRoleOperation(
|
||||
'update',
|
||||
'更新角色 角色A',
|
||||
{
|
||||
targetId: 'role_a',
|
||||
targetName: '角色A',
|
||||
beforeState,
|
||||
afterState,
|
||||
req: mockReq,
|
||||
metadata: { oldRoleName: '旧角色', oldPermissions: ['read'] }
|
||||
}
|
||||
);
|
||||
|
||||
const logs = await OperationLog.findAll({
|
||||
where: { module: 'role', operationType: 'update' }
|
||||
});
|
||||
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].beforeState.roleName).toBe('旧角色');
|
||||
expect(logs[0].afterState.roleName).toBe('新角色');
|
||||
});
|
||||
|
||||
test('应该记录角色删除日志', async () => {
|
||||
const mockReq = {
|
||||
user: { userId: 'admin', realName: '管理员' },
|
||||
headers: {}
|
||||
};
|
||||
|
||||
await logRoleOperation(
|
||||
'delete',
|
||||
'删除角色 测试角色B',
|
||||
{
|
||||
targetId: 'role_b',
|
||||
targetName: '测试角色B',
|
||||
beforeState: { roleName: '测试角色B', userCount: 0 },
|
||||
req: mockReq,
|
||||
metadata: { userCount: 0 }
|
||||
}
|
||||
);
|
||||
|
||||
const logs = await OperationLog.findAll({
|
||||
where: { module: 'role', operationType: 'delete' }
|
||||
});
|
||||
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].metadata.userCount).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,348 @@
|
||||
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,
|
||||
result
|
||||
} = 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}%` } },
|
||||
{ operatorName: { [Op.like]: `%${keyword}%` } }
|
||||
];
|
||||
}
|
||||
|
||||
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,
|
||||
limit
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
logs
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取操作日志失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取操作日志失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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,
|
||||
message: '日志记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: log
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取操作日志详情失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取操作日志详情失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.use('/api/operation-logs', operationLogsRouter);
|
||||
|
||||
return app;
|
||||
};
|
||||
|
||||
describe('OperationLogs API 路由测试', () => {
|
||||
let app;
|
||||
let authToken;
|
||||
const testUser = {
|
||||
userId: 'test_user_001',
|
||||
username: 'testuser',
|
||||
realName: '测试用户',
|
||||
roleName: '管理员'
|
||||
};
|
||||
|
||||
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,
|
||||
result: 'success'
|
||||
};
|
||||
return await OperationLog.create({ ...defaultData, ...data });
|
||||
};
|
||||
|
||||
describe('GET /api/operation-logs', () => {
|
||||
test('未授权访问应该返回 401', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/operation-logs')
|
||||
.expect(401);
|
||||
|
||||
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 () => {
|
||||
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' });
|
||||
|
||||
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);
|
||||
expect(response.body.data.logs.every(log => log.operationDescription.includes('SERVER'))).toBe(true);
|
||||
});
|
||||
|
||||
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',
|
||||
recordId: 'OPLOG_COMB_1'
|
||||
});
|
||||
await createTestLog({
|
||||
module: 'device',
|
||||
operationType: 'update',
|
||||
result: 'success',
|
||||
recordId: 'OPLOG_COMB_2'
|
||||
});
|
||||
await createTestLog({
|
||||
module: 'user',
|
||||
operationType: 'create',
|
||||
result: 'success',
|
||||
recordId: 'OPLOG_COMB_3'
|
||||
});
|
||||
|
||||
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: '新名称' },
|
||||
metadata: { customField: '自定义值' }
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
let isClosed = false;
|
||||
|
||||
const { sequelize } = require('../db');
|
||||
|
||||
beforeAll(async () => {
|
||||
try {
|
||||
if (!isClosed) {
|
||||
await sequelize.sync({ force: true });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Setup beforeAll error:', error);
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
afterAll(async () => {
|
||||
try {
|
||||
if (!isClosed) {
|
||||
isClosed = true;
|
||||
await sequelize.close();
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore close errors
|
||||
}
|
||||
}, 30000);
|
||||
@@ -0,0 +1,4 @@
|
||||
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:';
|
||||
Reference in New Issue
Block a user