302 lines
9.6 KiB
TypeScript
302 lines
9.6 KiB
TypeScript
/**
|
||
* 后端 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()
|