302 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 端点的功能
*/
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<string, string> = {}
) {
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<void>) {
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()