/** * 后端 API 测试脚本 * * 测试所有主要 API 端点的功能 */ const BASE_URL = 'http://localhost:3000/api' let authToken = '' // 测试用户信息(登录使用 identifier 字段) const TEST_USER = { identifier: 'admin@furniture3d.com', // 可以是邮箱或姓名 password: 'Admin123456!', // 默认种子数据密码 } // 颜色输出 const colors = { green: (s: string) => `\x1b[32m${s}\x1b[0m`, red: (s: string) => `\x1b[31m${s}\x1b[0m`, yellow: (s: string) => `\x1b[33m${s}\x1b[0m`, cyan: (s: string) => `\x1b[36m${s}\x1b[0m`, } // 辅助函数 async function request( method: string, endpoint: string, data?: unknown, headers: Record = {} ) { const url = `${BASE_URL}${endpoint}` const options: RequestInit = { method, headers: { 'Content-Type': 'application/json', ...(authToken ? { Cookie: `auth-token=${authToken}` } : {}), ...headers, }, } if (data && method !== 'GET') { options.body = JSON.stringify(data) } const response = await fetch(url, options) const json = await response.json().catch(() => ({})) return { status: response.status, ok: response.ok, data: json, headers: Object.fromEntries(response.headers.entries()), } } // 测试结果收集 const results: { name: string; passed: boolean; error?: string; duration: number }[] = [] async function test(name: string, fn: () => Promise) { const start = Date.now() try { await fn() results.push({ name, passed: true, duration: Date.now() - start }) console.log(` ${colors.green('✓')} ${name} (${Date.now() - start}ms)`) } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) results.push({ name, passed: false, error: errorMessage, duration: Date.now() - start }) console.log(` ${colors.red('✗')} ${name}`) console.log(` ${colors.red(errorMessage)}`) } } function assert(condition: boolean, message: string) { if (!condition) { throw new Error(message) } } // ==================== 测试用例 ==================== async function testHealth() { console.log(colors.cyan('\n🏥 健康检查测试')) await test('GET /health - 服务健康检查', async () => { const res = await request('GET', '/health') assert(res.ok, `期望状态码 200,实际 ${res.status}`) assert(res.data.status === 'healthy' || res.data.status === 'ok', `期望 status 为 healthy 或 ok,实际 ${res.data.status}`) }) } async function testAuth() { console.log(colors.cyan('\n🔐 认证测试')) await test('POST /auth/login - 用户登录', async () => { const res = await request('POST', '/auth/login', TEST_USER) assert(res.ok, `登录失败: ${JSON.stringify(res.data)}`) // 从 Set-Cookie 获取 token const setCookie = res.headers['set-cookie'] || '' const tokenMatch = setCookie.match(/auth-token=([^;]+)/) if (tokenMatch) { authToken = tokenMatch[1] } assert(res.data.user || res.data.success, '登录响应应包含用户信息') }) await test('POST /auth/login - 错误密码', async () => { const res = await request('POST', '/auth/login', { identifier: TEST_USER.identifier, password: 'wrongpassword', }) assert(!res.ok, '错误密码应该返回错误') assert(res.status === 401 || res.status === 400, `期望 401 或 400,实际 ${res.status}`) }) } async function testProducts() { console.log(colors.cyan('\n📦 产品 API 测试')) let firstProductId = '' await test('GET /products - 获取产品列表', async () => { const res = await request('GET', '/products') assert(res.ok, `获取产品列表失败: ${JSON.stringify(res.data)}`) assert(Array.isArray(res.data.products), '响应应包含 products 数组') assert(res.data.products.length > 0, '产品列表不应为空') firstProductId = res.data.products[0].id console.log(` 找到 ${res.data.products.length} 个产品`) }) await test('GET /products - 分页测试', async () => { const res = await request('GET', '/products?limit=5') assert(res.ok, `分页查询失败: ${JSON.stringify(res.data)}`) assert(res.data.products.length <= 5, '返回数量应不超过 limit') assert('hasMore' in res.data, '响应应包含 hasMore 字段') }) await test('GET /products - 搜索测试', async () => { const res = await request('GET', '/products?search=SF-001') assert(res.ok, `搜索失败: ${JSON.stringify(res.data)}`) if (res.data.products.length > 0) { const found = res.data.products.some((product: unknown) => { if (typeof product !== 'object' || product === null) { return false } const typedProduct = product as { modelNumber?: string; name?: string } return ( typedProduct.modelNumber?.includes('SF-001') || typedProduct.name?.includes('SF-001') ) }) assert(found, '搜索结果应包含匹配项') } }) await test('GET /products/[id] - 获取单个产品', async () => { if (!firstProductId) { throw new Error('没有可用的产品 ID') } const res = await request('GET', `/products/${firstProductId}`) assert(res.ok, `获取产品详情失败: ${JSON.stringify(res.data)}`) assert(res.data.product || res.data.id, '响应应包含产品信息') }) } async function testSKUs() { console.log(colors.cyan('\n🏷️ SKU API 测试')) // 先获取一个 SKU ID const productsRes = await request('GET', '/products?limit=1') const product = productsRes.data.products?.[0] const skuId = product?.skus?.[0]?.id if (!skuId) { console.log(colors.yellow(' ⚠ 没有可用的 SKU,跳过 SKU 测试')) return } await test('GET /skus/[id] - 获取 SKU 详情', async () => { const res = await request('GET', `/skus/${skuId}`) assert(res.ok, `获取 SKU 详情失败: ${JSON.stringify(res.data)}`) assert(res.data.sku, '响应应包含 sku 对象') assert(res.data.sku.skuCode, 'SKU 应有 skuCode') }) await test('PUT /skus/[id] - 更新 SKU', async () => { const newStock = Math.floor(Math.random() * 100) const res = await request('PUT', `/skus/${skuId}`, { stock: newStock, }) assert(res.ok, `更新 SKU 失败: ${JSON.stringify(res.data)}`) // 验证更新 const verifyRes = await request('GET', `/skus/${skuId}`) assert(verifyRes.data.sku.stock === newStock, `库存更新失败,期望 ${newStock},实际 ${verifyRes.data.sku.stock}`) }) } async function testShare() { console.log(colors.cyan('\n🔗 分享 API 测试')) // 获取一个产品 ID const productsRes = await request('GET', '/products?limit=1') const productId = productsRes.data.products?.[0]?.id if (!productId) { console.log(colors.yellow(' ⚠ 没有可用的产品,跳过分享测试')) return } await test('POST /share - 创建分享链接', async () => { const res = await request('POST', '/share', { productId, expiresInDays: 1, // 1 天 }) assert(res.ok, `创建分享链接失败: ${JSON.stringify(res.data)}`) assert(res.data.token || res.data.shareLink, '响应应包含 token 或 shareLink') }) } async function testUsers() { console.log(colors.cyan('\n👥 用户 API 测试')) await test('GET /users - 获取用户列表', async () => { const res = await request('GET', '/users') assert(res.ok, `获取用户列表失败: ${JSON.stringify(res.data)}`) assert(Array.isArray(res.data.users) || Array.isArray(res.data), '响应应包含用户数组') }) } async function testReports() { console.log(colors.cyan('\n📊 报表 API 测试')) await test('GET /reports/product-changes - 产品变更报表', async () => { const res = await request('GET', '/reports/product-changes') // 报表 API 可能需要特殊权限,所以只检查是否有响应 assert(res.status !== 500, `服务器错误: ${JSON.stringify(res.data)}`) }) await test('GET /reports/user-activity - 用户活动报表', async () => { const res = await request('GET', '/reports/user-activity') assert(res.status !== 500, `服务器错误: ${JSON.stringify(res.data)}`) }) } async function testLogs() { console.log(colors.cyan('\n📝 日志 API 测试')) await test('GET /logs - 获取操作日志', async () => { const res = await request('GET', '/logs') assert(res.status !== 500, `服务器错误: ${JSON.stringify(res.data)}`) }) } // ==================== 运行测试 ==================== async function main() { console.log(colors.cyan('🚀 开始后端 API 测试')) console.log(` 目标: ${BASE_URL}`) console.log(` 时间: ${new Date().toLocaleString()}`) try { await testHealth() await testAuth() await testProducts() await testSKUs() await testShare() await testUsers() await testReports() await testLogs() } catch (error) { console.error(colors.red('\n测试执行出错:'), error) } // 输出汇总 console.log(colors.cyan('\n📋 测试汇总')) const passed = results.filter(r => r.passed).length const failed = results.filter(r => !r.passed).length const totalDuration = results.reduce((sum, r) => sum + r.duration, 0) console.log(` 总计: ${results.length} 个测试`) console.log(` ${colors.green(`通过: ${passed}`)}`) if (failed > 0) { console.log(` ${colors.red(`失败: ${failed}`)}`) console.log('\n失败的测试:') results.filter(r => !r.passed).forEach(r => { console.log(` - ${r.name}: ${r.error}`) }) } console.log(` 耗时: ${totalDuration}ms`) // 退出码 process.exit(failed > 0 ? 1 : 0) } main()