1. 新增操作日志记录功能,记录关键操作 2. 实现危险操作确认对话框,防止误删 3. 添加业务和库房管理模块 4. 支持设备标记为空闲状态 5. 完善API文档和健康检查 6. 优化前端删除操作的确认流程 7. 添加Swagger API文档支持 8. 实现设备与业务的关联功能 9. 改进设备模型,添加空闲相关字段 10. 优化用户、角色管理操作日志
349 lines
10 KiB
JavaScript
349 lines
10 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|