- 文件上传验证: ModelUploader组件增强、上传API错误处理 - GLTF解析器: 增强节点提取和元数据 - 板材编辑: PanelEditor客户端验证 + panels API服务端验证 - 装配步骤编辑: 修复未使用代码,添加预览按钮 - 装配动画播放: AssemblyPlayer步骤导航边界检查优化 - 分享链接: 完整CRUD API(创建、获取、删除),含验证和过期检查 跳过可选测试任务以加快MVP开发
440 lines
12 KiB
TypeScript
440 lines
12 KiB
TypeScript
/**
|
|
* 操作日志 API 集成测试
|
|
*
|
|
* 测试范围:
|
|
* - 日志记录
|
|
* - 日志查询
|
|
* - 日志筛选
|
|
* - 日志统计
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';
|
|
import { PrismaClient, ActivityType } from '@prisma/client';
|
|
import { setupTestEnvironment, cleanDatabase } from '../helpers/test-utils';
|
|
|
|
const prisma = new PrismaClient();
|
|
|
|
describe('操作日志 API 测试', () => {
|
|
let testEnv: any;
|
|
|
|
beforeAll(async () => {
|
|
await cleanDatabase();
|
|
testEnv = await setupTestEnvironment();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await cleanDatabase();
|
|
await prisma.$disconnect();
|
|
});
|
|
|
|
describe('日志记录', () => {
|
|
it('应该能记录用户登录日志', async () => {
|
|
const log = await prisma.activityLog.create({
|
|
data: {
|
|
userId: testEnv.users.designer.id,
|
|
type: ActivityType.USER_LOGIN,
|
|
action: '用户登录',
|
|
metadata: {
|
|
ip: '192.168.1.100',
|
|
userAgent: 'Mozilla/5.0',
|
|
device: 'Desktop'
|
|
}
|
|
}
|
|
});
|
|
|
|
expect(log).toBeDefined();
|
|
expect(log.type).toBe(ActivityType.USER_LOGIN);
|
|
expect(log.metadata).toBeDefined();
|
|
});
|
|
|
|
it('应该能记录产品创建日志', async () => {
|
|
const log = await prisma.activityLog.create({
|
|
data: {
|
|
userId: testEnv.users.designer.id,
|
|
type: ActivityType.PRODUCT_CREATE,
|
|
targetType: 'Product',
|
|
targetId: testEnv.product.id,
|
|
action: '创建产品',
|
|
changes: {
|
|
modelNumber: testEnv.product.modelNumber,
|
|
name: testEnv.product.name
|
|
}
|
|
}
|
|
});
|
|
|
|
expect(log.targetType).toBe('Product');
|
|
expect(log.targetId).toBe(testEnv.product.id);
|
|
expect(log.changes).toBeDefined();
|
|
});
|
|
|
|
it('应该能记录产品更新日志', async () => {
|
|
const log = await prisma.activityLog.create({
|
|
data: {
|
|
userId: testEnv.users.designer.id,
|
|
type: ActivityType.PRODUCT_UPDATE,
|
|
targetType: 'Product',
|
|
targetId: testEnv.product.id,
|
|
action: '更新产品信息',
|
|
changes: {
|
|
before: { name: '旧名称' },
|
|
after: { name: '新名称' }
|
|
}
|
|
}
|
|
});
|
|
|
|
expect(log.type).toBe(ActivityType.PRODUCT_UPDATE);
|
|
const changes = log.changes as any;
|
|
expect(changes.before).toBeDefined();
|
|
expect(changes.after).toBeDefined();
|
|
});
|
|
|
|
it('应该能记录 SKU 交接日志', async () => {
|
|
const log = await prisma.activityLog.create({
|
|
data: {
|
|
userId: testEnv.users.admin.id,
|
|
type: ActivityType.SKU_TRANSFER,
|
|
targetType: 'ProductSKU',
|
|
targetId: testEnv.sku.id,
|
|
action: 'SKU 交接',
|
|
changes: {
|
|
fromUserId: testEnv.users.designer.id,
|
|
toUserId: testEnv.users.teamLeader.id,
|
|
reason: '人员调整'
|
|
}
|
|
}
|
|
});
|
|
|
|
expect(log.type).toBe(ActivityType.SKU_TRANSFER);
|
|
const changes = log.changes as any;
|
|
expect(changes.fromUserId).toBeDefined();
|
|
expect(changes.toUserId).toBeDefined();
|
|
});
|
|
|
|
it('应该能记录文件上传日志', async () => {
|
|
const log = await prisma.activityLog.create({
|
|
data: {
|
|
userId: testEnv.users.designer.id,
|
|
type: ActivityType.FILE_UPLOAD,
|
|
action: '上传 3D 模型',
|
|
metadata: {
|
|
fileName: 'sofa-model.glb',
|
|
fileSize: 1024000,
|
|
fileType: 'model/gltf-binary'
|
|
}
|
|
}
|
|
});
|
|
|
|
expect(log.type).toBe(ActivityType.FILE_UPLOAD);
|
|
const metadata = log.metadata as any;
|
|
expect(metadata.fileName).toBe('sofa-model.glb');
|
|
});
|
|
|
|
it('应该能记录分享链接创建日志', async () => {
|
|
const log = await prisma.activityLog.create({
|
|
data: {
|
|
userId: testEnv.users.designer.id,
|
|
type: ActivityType.SHARE_LINK_CREATE,
|
|
targetType: 'ShareLink',
|
|
targetId: 'share-link-id',
|
|
action: '创建分享链接',
|
|
metadata: {
|
|
productId: testEnv.product.id,
|
|
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString()
|
|
}
|
|
}
|
|
});
|
|
|
|
expect(log.type).toBe(ActivityType.SHARE_LINK_CREATE);
|
|
});
|
|
});
|
|
|
|
describe('日志查询', () => {
|
|
beforeAll(async () => {
|
|
// 创建测试日志数据
|
|
const logTypes = [
|
|
ActivityType.USER_LOGIN,
|
|
ActivityType.PRODUCT_CREATE,
|
|
ActivityType.PRODUCT_UPDATE,
|
|
ActivityType.SKU_CREATE,
|
|
ActivityType.FILE_UPLOAD
|
|
];
|
|
|
|
for (const type of logTypes) {
|
|
await prisma.activityLog.create({
|
|
data: {
|
|
userId: testEnv.users.designer.id,
|
|
type,
|
|
action: `测试操作 - ${type}`
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
it('应该能查询所有日志', async () => {
|
|
const logs = await prisma.activityLog.findMany({
|
|
include: {
|
|
user: {
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
email: true
|
|
}
|
|
}
|
|
},
|
|
orderBy: {
|
|
createdAt: 'desc'
|
|
}
|
|
});
|
|
|
|
expect(logs.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('应该能查询指定用户的日志', async () => {
|
|
const userLogs = await prisma.activityLog.findMany({
|
|
where: {
|
|
userId: testEnv.users.designer.id
|
|
}
|
|
});
|
|
|
|
expect(userLogs.length).toBeGreaterThan(0);
|
|
userLogs.forEach(log => {
|
|
expect(log.userId).toBe(testEnv.users.designer.id);
|
|
});
|
|
});
|
|
|
|
it('应该能按操作类型筛选', async () => {
|
|
const loginLogs = await prisma.activityLog.findMany({
|
|
where: {
|
|
type: ActivityType.USER_LOGIN
|
|
}
|
|
});
|
|
|
|
loginLogs.forEach(log => {
|
|
expect(log.type).toBe(ActivityType.USER_LOGIN);
|
|
});
|
|
});
|
|
|
|
it('应该能按目标类型筛选', async () => {
|
|
const productLogs = await prisma.activityLog.findMany({
|
|
where: {
|
|
targetType: 'Product'
|
|
}
|
|
});
|
|
|
|
productLogs.forEach(log => {
|
|
expect(log.targetType).toBe('Product');
|
|
});
|
|
});
|
|
|
|
it('应该能查询指定对象的日志', async () => {
|
|
const objectLogs = await prisma.activityLog.findMany({
|
|
where: {
|
|
targetType: 'Product',
|
|
targetId: testEnv.product.id
|
|
},
|
|
orderBy: {
|
|
createdAt: 'desc'
|
|
}
|
|
});
|
|
|
|
objectLogs.forEach(log => {
|
|
expect(log.targetId).toBe(testEnv.product.id);
|
|
});
|
|
});
|
|
|
|
it('应该能按时间范围查询', async () => {
|
|
const yesterday = new Date();
|
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
|
|
const recentLogs = await prisma.activityLog.findMany({
|
|
where: {
|
|
createdAt: {
|
|
gte: yesterday
|
|
}
|
|
}
|
|
});
|
|
|
|
expect(recentLogs.length).toBeGreaterThan(0);
|
|
recentLogs.forEach(log => {
|
|
expect(log.createdAt.getTime()).toBeGreaterThanOrEqual(yesterday.getTime());
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('日志分页', () => {
|
|
it('应该支持分页查询', async () => {
|
|
const pageSize = 10;
|
|
const page1 = await prisma.activityLog.findMany({
|
|
take: pageSize,
|
|
skip: 0,
|
|
orderBy: {
|
|
createdAt: 'desc'
|
|
}
|
|
});
|
|
|
|
expect(page1.length).toBeLessThanOrEqual(pageSize);
|
|
});
|
|
|
|
it('应该能获取总数', async () => {
|
|
const total = await prisma.activityLog.count();
|
|
|
|
expect(total).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('应该能获取筛选后的总数', async () => {
|
|
const count = await prisma.activityLog.count({
|
|
where: {
|
|
userId: testEnv.users.designer.id
|
|
}
|
|
});
|
|
|
|
expect(count).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('日志统计', () => {
|
|
it('应该能统计各操作类型的数量', async () => {
|
|
const typeCounts = await prisma.activityLog.groupBy({
|
|
by: ['type'],
|
|
_count: true
|
|
});
|
|
|
|
expect(typeCounts.length).toBeGreaterThan(0);
|
|
typeCounts.forEach(item => {
|
|
expect(item.type).toBeDefined();
|
|
expect(item._count).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
it('应该能统计用户活跃度', async () => {
|
|
const userActivity = await prisma.activityLog.groupBy({
|
|
by: ['userId'],
|
|
_count: true,
|
|
orderBy: {
|
|
_count: {
|
|
userId: 'desc'
|
|
}
|
|
}
|
|
});
|
|
|
|
expect(userActivity.length).toBeGreaterThan(0);
|
|
userActivity.forEach(item => {
|
|
expect(item.userId).toBeDefined();
|
|
expect(item._count).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
it('应该能统计每日操作量', async () => {
|
|
const today = new Date();
|
|
today.setHours(0, 0, 0, 0);
|
|
|
|
const todayCount = await prisma.activityLog.count({
|
|
where: {
|
|
createdAt: {
|
|
gte: today
|
|
}
|
|
}
|
|
});
|
|
|
|
expect(todayCount).toBeGreaterThanOrEqual(0);
|
|
});
|
|
});
|
|
|
|
describe('安全日志', () => {
|
|
it('应该能记录登录失败日志', async () => {
|
|
const log = await prisma.activityLog.create({
|
|
data: {
|
|
userId: testEnv.users.designer.id,
|
|
type: ActivityType.USER_LOGIN_FAILED,
|
|
action: '登录失败',
|
|
metadata: {
|
|
ip: '192.168.1.100',
|
|
reason: '密码错误',
|
|
attemptCount: 3
|
|
}
|
|
}
|
|
});
|
|
|
|
expect(log.type).toBe(ActivityType.USER_LOGIN_FAILED);
|
|
const metadata = log.metadata as any;
|
|
expect(metadata.reason).toBe('密码错误');
|
|
});
|
|
|
|
it('应该能查询异常登录记录', async () => {
|
|
const failedLogins = await prisma.activityLog.findMany({
|
|
where: {
|
|
type: ActivityType.USER_LOGIN_FAILED
|
|
},
|
|
orderBy: {
|
|
createdAt: 'desc'
|
|
}
|
|
});
|
|
|
|
failedLogins.forEach(log => {
|
|
expect(log.type).toBe(ActivityType.USER_LOGIN_FAILED);
|
|
});
|
|
});
|
|
|
|
it('应该能记录数据导出日志', async () => {
|
|
const log = await prisma.activityLog.create({
|
|
data: {
|
|
userId: testEnv.users.admin.id,
|
|
type: ActivityType.DATA_EXPORT,
|
|
action: '导出产品数据',
|
|
metadata: {
|
|
exportType: 'products',
|
|
recordCount: 100,
|
|
format: 'xlsx'
|
|
}
|
|
}
|
|
});
|
|
|
|
expect(log.type).toBe(ActivityType.DATA_EXPORT);
|
|
});
|
|
});
|
|
|
|
describe('日志保留策略', () => {
|
|
it('应该能删除过期日志', async () => {
|
|
// 创建一个旧日志
|
|
const oldDate = new Date();
|
|
oldDate.setMonth(oldDate.getMonth() - 7); // 7个月前
|
|
|
|
const oldLog = await prisma.activityLog.create({
|
|
data: {
|
|
userId: testEnv.users.designer.id,
|
|
type: ActivityType.USER_LOGIN,
|
|
action: '旧日志',
|
|
createdAt: oldDate
|
|
}
|
|
});
|
|
|
|
// 删除 6 个月前的日志
|
|
const sixMonthsAgo = new Date();
|
|
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);
|
|
|
|
const deleteResult = await prisma.activityLog.deleteMany({
|
|
where: {
|
|
createdAt: {
|
|
lt: sixMonthsAgo
|
|
}
|
|
}
|
|
});
|
|
|
|
expect(deleteResult.count).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('应该能统计日志存储量', async () => {
|
|
const totalLogs = await prisma.activityLog.count();
|
|
const last30Days = await prisma.activityLog.count({
|
|
where: {
|
|
createdAt: {
|
|
gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
|
|
}
|
|
}
|
|
});
|
|
|
|
expect(totalLogs).toBeGreaterThanOrEqual(last30Days);
|
|
});
|
|
});
|
|
});
|