- 创建统一的下载错误处理模块 (lib/download-errors.ts) - 实现13种错误类型分类和中文错误消息 - 后端统一使用 createDownloadError 创建标准化错误响应 - 前端统一使用 parseDownloadError 解析错误并显示 - 修复正式系统数据库Schema,添加产品全案相关字段和表 - 执行数据库迁移:添加 task_no, task_status, task_fetched_at 字段 - 创建 material_assets, api_keys, api_call_logs 表
502 lines
15 KiB
TypeScript
502 lines
15 KiB
TypeScript
/**
|
||
* 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();
|
||
});
|
||
|