gongxiangp2/scripts/test-api-stages-3-4.ts
chenpei ea9076df45 feat: 实现统一的下载错误处理系统并修复产品全案数据库Schema
- 创建统一的下载错误处理模块 (lib/download-errors.ts)
- 实现13种错误类型分类和中文错误消息
- 后端统一使用 createDownloadError 创建标准化错误响应
- 前端统一使用 parseDownloadError 解析错误并显示
- 修复正式系统数据库Schema,添加产品全案相关字段和表
- 执行数据库迁移:添加 task_no, task_status, task_fetched_at 字段
- 创建 material_assets, api_keys, api_call_logs 表
2026-01-05 17:52:42 +08:00

502 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* API测试脚本:阶段三和阶段四功能测试
* 直接在影子系统上测试后端API功能
*
* 使用方法:
* DATABASE_URL=postgresql://... npx tsx scripts/test-api-stages-3-4.ts
*/
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const SHADOW_SYSTEM_URL = process.env.SHADOW_SYSTEM_URL || 'http://192.168.58.20:5320';
const TEST_ADMIN_EMAIL = process.env.TEST_ADMIN_EMAIL || '';
const TEST_ADMIN_PASSWORD = process.env.TEST_ADMIN_PASSWORD || '';
interface TestResult {
name: string;
passed: boolean;
error?: string;
details?: any;
}
const results: TestResult[] = [];
async function test(name: string, testFn: () => Promise<any>): Promise<void> {
try {
console.log(`\n🧪 测试: ${name}`);
const details = await testFn();
results.push({ name, passed: true, details });
console.log(` ✅ 通过`);
if (details && typeof details === 'object') {
const detailsStr = JSON.stringify(details, null, 2);
const lines = detailsStr.split('\n');
console.log(` 详情:`, lines.slice(0, 15).join('\n'));
if (lines.length > 15) {
console.log(` ... (还有 ${lines.length - 15} 行)`);
}
}
} catch (error: any) {
results.push({ name, passed: false, error: error.message });
console.log(` ❌ 失败: ${error.message}`);
if (error.stack) {
console.log(` 堆栈:`, error.stack.split('\n').slice(0, 3).join('\n'));
}
}
}
// 获取认证token(模拟登录)
async function getAuthToken(): Promise<string | null> {
if (!TEST_ADMIN_EMAIL || !TEST_ADMIN_PASSWORD) {
console.log(' ⚠️ 未设置TEST_ADMIN_EMAIL和TEST_ADMIN_PASSWORD,跳过需要认证的测试');
return null;
}
try {
const response = await fetch(`${SHADOW_SYSTEM_URL}/api/auth/callback/credentials`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: TEST_ADMIN_EMAIL,
password: TEST_ADMIN_PASSWORD,
}),
});
// NextAuth的登录流程比较复杂,这里简化处理
// 实际测试中可能需要通过浏览器登录获取session cookie
return null;
} catch (error) {
return null;
}
}
async function main() {
console.log('========================================');
console.log('API功能测试:阶段三和阶段四');
console.log('========================================');
console.log(`测试目标: ${SHADOW_SYSTEM_URL}`);
console.log(`数据库: ${process.env.DATABASE_URL?.split('@')[1] || '未设置'}`);
console.log('');
// ========== 阶段三测试:部门层级结构 API ==========
// 测试1: 获取部门列表(检查返回的字段)
await test('阶段三:GET /api/departments - 检查返回字段', async () => {
const response = await fetch(`${SHADOW_SYSTEM_URL}/api/departments`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const departments = await response.json();
if (!Array.isArray(departments)) {
throw new Error('返回数据不是数组');
}
if (departments.length === 0) {
return { message: '没有部门数据', departments: [] };
}
// 检查第一个部门是否有新字段
const firstDept = departments[0];
const hasNewFields =
'code' in firstDept ||
'parentId' in firstDept ||
'level' in firstDept ||
'status' in firstDept ||
'managerId' in firstDept;
return {
totalDepartments: departments.length,
sampleDepartment: {
id: firstDept.id,
name: firstDept.name,
hasCode: 'code' in firstDept,
hasParentId: 'parentId' in firstDept,
hasLevel: 'level' in firstDept,
hasStatus: 'status' in firstDept,
hasManagerId: 'managerId' in firstDept,
code: firstDept.code,
parentId: firstDept.parentId,
level: firstDept.level,
status: firstDept.status,
},
hasNewFields,
};
});
// 测试2: 通过数据库查询验证阶段三字段(因为API可能不返回所有字段)
await test('阶段三:数据库验证 - 部门层级字段', async () => {
// 使用原生SQL查询避免TypeScript类型错误
const dept = await prisma.$queryRaw<Array<{
id: string;
name: string;
code: string | null;
parentId: string | null;
level: number;
managerId: string | null;
status: string;
description: string | null;
sortOrder: number;
updatedAt: Date;
}>>`
SELECT id, name, code, "parentId", level, "managerId", status, description, "sortOrder", "updatedAt"
FROM departments
LIMIT 1
`;
if (!dept || dept.length === 0) {
return { message: '没有部门数据' };
}
const firstDept = dept[0];
return {
sampleDepartment: {
name: firstDept.name,
code: firstDept.code,
parentId: firstDept.parentId,
level: firstDept.level,
status: firstDept.status,
managerId: firstDept.managerId,
description: firstDept.description,
sortOrder: firstDept.sortOrder,
hasUpdatedAt: firstDept.updatedAt !== null,
},
allFieldsPresent: true,
};
});
// 测试3: 测试创建带父部门的部门(需要认证,这里只测试逻辑)
await test('阶段三:POST /api/departments - 创建部门(逻辑验证)', async () => {
// 使用原生SQL查询避免TypeScript类型错误
const parentDept = await prisma.$queryRaw<Array<{
id: string;
name: string;
level: number;
}>>`
SELECT id, name, level
FROM departments
LIMIT 1
`;
if (!parentDept || parentDept.length === 0) {
return { message: '没有父部门可用于测试' };
}
const firstDept = parentDept[0];
// 测试数据(不实际创建,只验证逻辑)
const testData = {
name: `测试子部门_${Date.now()}`,
parentId: firstDept.id,
description: '测试子部门描述',
};
return {
testData,
parentDepartment: {
id: firstDept.id,
name: firstDept.name,
level: firstDept.level,
},
expectedLevel: firstDept.level + 1,
note: '实际创建需要管理员认证,这里只验证数据结构',
};
});
// 测试4: 验证部门层级关系查询
await test('阶段三:数据库查询 - 部门层级关系', async () => {
// 使用原生SQL查询避免TypeScript类型错误
// 查找有父部门的部门
const childDepts = await prisma.$queryRaw<Array<{
id: string;
name: string;
level: number;
parentId: string;
parentName: string;
parentLevel: number;
}>>`
SELECT
d.id, d.name, d.level, d."parentId",
p.name as "parentName", p.level as "parentLevel"
FROM departments d
LEFT JOIN departments p ON d."parentId" = p.id
WHERE d."parentId" IS NOT NULL
LIMIT 3
`;
// 查找没有父部门的部门(一级部门)
const topLevelDepts = await prisma.$queryRaw<Array<{
id: string;
name: string;
level: number;
}>>`
SELECT id, name, level
FROM departments
WHERE "parentId" IS NULL
LIMIT 3
`;
// 统计子部门数量
const childrenCounts = await prisma.$queryRaw<Array<{
parentId: string;
count: bigint;
}>>`
SELECT "parentId", COUNT(*) as count
FROM departments
WHERE "parentId" IS NOT NULL
GROUP BY "parentId"
LIMIT 3
`;
return {
departmentsWithParent: childDepts.length,
sampleChildDept: childDepts.length > 0 ? {
name: childDepts[0].name,
level: childDepts[0].level,
parentName: childDepts[0].parentName,
parentLevel: childDepts[0].parentLevel,
} : null,
topLevelDepartments: topLevelDepts.length,
sampleTopLevelDept: topLevelDepts.length > 0 ? {
name: topLevelDepts[0].name,
level: topLevelDepts[0].level,
} : null,
};
});
// ========== 阶段四测试:角色枚举扩展 API ==========
// 测试5: 检查用户角色(通过数据库)
await test('阶段四:数据库验证 - 用户角色枚举', async () => {
const roleCounts = await prisma.$queryRaw<Array<{ role: string; count: bigint }>>`
SELECT role, COUNT(*) as count
FROM users
GROUP BY role
ORDER BY count DESC
`;
const roles = roleCounts.map(r => r.role);
const hasNewRoles = roles.includes('DEPT_ADMIN') || roles.includes('SYSTEM_ADMIN');
return {
roleDistribution: roleCounts.map(r => ({
role: r.role,
count: Number(r.count),
})),
hasNewRoles,
supportedRoles: ['USER', 'ADMIN', 'DEPT_ADMIN', 'SYSTEM_ADMIN'],
};
});
// 测试6: 测试设置部门管理员API(逻辑验证)
await test('阶段四:POST /api/admin/users/set-department-manager - 设置部门管理员(逻辑验证)', async () => {
// 找一个普通用户
const user = await prisma.user.findFirst({
where: {
role: 'USER',
status: 'active',
},
select: {
id: true,
email: true,
username: true,
role: true,
},
});
// 使用原生SQL查询部门(避免TypeScript类型错误)
const department = await prisma.$queryRaw<Array<{
id: string;
name: string;
managerId: string | null;
}>>`
SELECT id, name, "managerId"
FROM departments
LIMIT 1
`;
if (!user || !department || department.length === 0) {
return { message: '没有足够的测试数据(需要至少1个普通用户和1个部门)' };
}
const firstDept = department[0];
return {
testData: {
userId: user.id,
departmentId: firstDept.id,
},
currentUserRole: user.role,
currentDepartmentManager: firstDept.managerId,
expectedNewRole: 'DEPT_ADMIN',
note: '实际设置需要管理员认证,这里只验证数据结构',
};
});
// 测试7: 验证部门管理员关联
await test('阶段四:数据库验证 - 部门管理员关联', async () => {
// 使用原生SQL查询避免TypeScript类型错误
const deptsWithManager = await prisma.$queryRaw<Array<{
departmentId: string;
departmentName: string;
managerId: string;
managerEmail: string;
managerUsername: string;
managerRole: string;
}>>`
SELECT
d.id as "departmentId",
d.name as "departmentName",
d."managerId",
u.email as "managerEmail",
u.username as "managerUsername",
u.role as "managerRole"
FROM departments d
INNER JOIN users u ON d."managerId" = u.id
WHERE d."managerId" IS NOT NULL
LIMIT 5
`;
return {
departmentsWithManager: deptsWithManager.length,
sampleDepartments: deptsWithManager.map(d => ({
departmentName: d.departmentName,
managerEmail: d.managerEmail,
managerRole: d.managerRole,
isValidRole: d.managerRole === 'DEPT_ADMIN' || d.managerRole === 'SYSTEM_ADMIN' || d.managerRole === 'ADMIN',
})),
};
});
// 测试8: 检查角色权限检查函数(通过查询验证)
await test('阶段四:权限检查 - 角色分布验证', async () => {
// 使用原生SQL查询统计不同角色的用户
const adminUsers = await prisma.$queryRaw<Array<{ count: bigint }>>`
SELECT COUNT(*) as count
FROM users
WHERE role IN ('ADMIN', 'SYSTEM_ADMIN')
`;
const deptAdminUsers = await prisma.$queryRaw<Array<{ count: bigint }>>`
SELECT COUNT(*) as count
FROM users
WHERE role = 'DEPT_ADMIN'
`;
const normalUsers = await prisma.$queryRaw<Array<{ count: bigint }>>`
SELECT COUNT(*) as count
FROM users
WHERE role = 'USER'
`;
// 检查部门管理员的角色一致性
const invalidDeptManagers = await prisma.$queryRaw<Array<{ count: bigint }>>`
SELECT COUNT(*) as count
FROM departments d
INNER JOIN users u ON d."managerId" = u.id
WHERE u.role NOT IN ('DEPT_ADMIN', 'SYSTEM_ADMIN', 'ADMIN')
`;
return {
userRoleDistribution: {
systemAdmins: Number(adminUsers[0]?.count || 0),
deptAdmins: Number(deptAdminUsers[0]?.count || 0),
normalUsers: Number(normalUsers[0]?.count || 0),
},
invalidDeptManagers: Number(invalidDeptManagers[0]?.count || 0),
note: invalidDeptManagers[0] && Number(invalidDeptManagers[0].count) > 0
? '发现部分部门管理员的角色不是DEPT_ADMIN/SYSTEM_ADMIN'
: '所有部门管理员的角色正确',
};
});
// 测试9: 综合统计
await test('综合统计', async () => {
const totalUsers = await prisma.user.count();
const totalDepts = await prisma.department.count();
const deptsByLevel = await prisma.$queryRaw<Array<{ level: number; count: bigint }>>`
SELECT level, COUNT(*) as count
FROM departments
GROUP BY level
ORDER BY level
`;
const deptsByStatus = await prisma.$queryRaw<Array<{ status: string; count: bigint }>>`
SELECT status, COUNT(*) as count
FROM departments
GROUP BY status
`;
const userRoles = await prisma.$queryRaw<Array<{ role: string; count: bigint }>>`
SELECT role, COUNT(*) as count
FROM users
GROUP BY role
`;
return {
users: {
total: totalUsers,
byRole: userRoles.map(r => ({
role: r.role,
count: Number(r.count),
})),
},
departments: {
total: totalDepts,
byLevel: deptsByLevel.map(l => ({
level: Number(l.level),
count: Number(l.count),
})),
byStatus: deptsByStatus.map(s => ({
status: s.status,
count: Number(s.count),
})),
},
};
});
// 输出测试结果
console.log('\n========================================');
console.log('测试结果汇总');
console.log('========================================');
const passed = results.filter(r => r.passed).length;
const failed = results.filter(r => !r.passed).length;
results.forEach(result => {
const icon = result.passed ? '✅' : '❌';
console.log(`${icon} ${result.name}`);
if (!result.passed && result.error) {
console.log(` 错误: ${result.error}`);
}
});
console.log('\n========================================');
console.log(`总计: ${results.length} 个测试`);
console.log(`✅ 通过: ${passed}`);
console.log(`❌ 失败: ${failed}`);
console.log('========================================\n');
if (failed > 0) {
console.log('⚠️ 部分测试失败,请检查上述问题');
process.exit(1);
} else {
console.log('✅ 所有测试通过!');
}
}
main()
.catch((error) => {
console.error('测试执行失败:', error);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});