373 lines
11 KiB
JavaScript
373 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
const http = require('http');
|
|
|
|
class APITester {
|
|
constructor(baseUrl) {
|
|
this.baseUrl = baseUrl;
|
|
this.cookies = {};
|
|
this.results = [];
|
|
}
|
|
|
|
async request(path, options = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
const url = new URL(path, this.baseUrl);
|
|
const opts = {
|
|
hostname: url.hostname,
|
|
port: url.port || 3000,
|
|
path: url.pathname + url.search,
|
|
method: options.method || 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...options.headers
|
|
}
|
|
};
|
|
|
|
// 添加 cookies
|
|
const cookieStr = Object.entries(this.cookies)
|
|
.map(([k, v]) => `${k}=${v}`)
|
|
.join('; ');
|
|
if (cookieStr) {
|
|
opts.headers.Cookie = cookieStr;
|
|
}
|
|
|
|
const req = http.request(opts, (res) => {
|
|
let data = '';
|
|
res.on('data', chunk => data += chunk);
|
|
res.on('end', () => {
|
|
// 保存 cookies
|
|
if (res.headers['set-cookie']) {
|
|
res.headers['set-cookie'].forEach(cookie => {
|
|
const [nameValue] = cookie.split(';');
|
|
const [name, value] = nameValue.split('=');
|
|
if (name && value) {
|
|
this.cookies[name.trim()] = value.trim();
|
|
}
|
|
});
|
|
}
|
|
|
|
let parsedData;
|
|
try {
|
|
parsedData = JSON.parse(data);
|
|
} catch {
|
|
parsedData = data;
|
|
}
|
|
|
|
resolve({
|
|
status: res.statusCode,
|
|
data: parsedData,
|
|
headers: res.headers
|
|
});
|
|
});
|
|
});
|
|
|
|
req.on('error', reject);
|
|
|
|
if (options.body) {
|
|
req.write(JSON.stringify(options.body));
|
|
}
|
|
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
log(emoji, title, status, message, data = null) {
|
|
const result = {
|
|
emoji,
|
|
title,
|
|
status,
|
|
message,
|
|
data
|
|
};
|
|
this.results.push(result);
|
|
|
|
console.log(`\n${emoji} ${title}`);
|
|
console.log(` 状态: ${status}`);
|
|
console.log(` ${message}`);
|
|
if (data) {
|
|
console.log(` 数据:`, JSON.stringify(data, null, 2).split('\n').map(l => ' ' + l).join('\n'));
|
|
}
|
|
}
|
|
|
|
async testRegister() {
|
|
try {
|
|
const email = `test-${Date.now()}@example.com`;
|
|
const response = await this.request('/api/auth/register', {
|
|
method: 'POST',
|
|
body: {
|
|
email,
|
|
password: 'password123',
|
|
name: '测试用户'
|
|
}
|
|
});
|
|
|
|
if (response.status === 200) {
|
|
this.log('✅', '用户注册', 'PASS', `成功注册用户: ${email}`, response.data);
|
|
this.testEmail = email;
|
|
return true;
|
|
} else {
|
|
this.log('❌', '用户注册', 'FAIL', `注册失败: ${response.status}`, response.data);
|
|
return false;
|
|
}
|
|
} catch (error) {
|
|
this.log('❌', '用户注册', 'ERROR', error.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async testGetCSRF() {
|
|
try {
|
|
const response = await this.request('/api/auth/csrf', {
|
|
method: 'GET'
|
|
});
|
|
|
|
if (response.status === 200 && response.data.csrfToken) {
|
|
this.csrfToken = response.data.csrfToken;
|
|
this.log('✅', '获取 CSRF Token', 'PASS', 'CSRF Token 获取成功');
|
|
return true;
|
|
} else {
|
|
this.log('❌', '获取 CSRF Token', 'FAIL', `状态码: ${response.status}`);
|
|
return false;
|
|
}
|
|
} catch (error) {
|
|
this.log('❌', '获取 CSRF Token', 'ERROR', error.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async testLogin() {
|
|
try {
|
|
const response = await this.request('/api/auth/callback/credentials', {
|
|
method: 'POST',
|
|
body: {
|
|
email: this.testEmail,
|
|
password: 'password123',
|
|
csrfToken: this.csrfToken,
|
|
callbackUrl: 'http://localhost:3000',
|
|
json: true
|
|
}
|
|
});
|
|
|
|
if (response.status === 200 || response.status === 302) {
|
|
this.log('✅', '用户登录', 'PASS', '登录成功,获取到 session');
|
|
return true;
|
|
} else {
|
|
this.log('❌', '用户登录', 'FAIL', `状态码: ${response.status}`, response.data);
|
|
return false;
|
|
}
|
|
} catch (error) {
|
|
this.log('❌', '用户登录', 'ERROR', error.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async testGetTrees() {
|
|
try {
|
|
const response = await this.request('/api/trees', {
|
|
method: 'GET'
|
|
});
|
|
|
|
if (response.status === 200) {
|
|
this.log('✅', '获取家族树列表', 'PASS', `成功获取 ${response.data.trees?.length || 0} 个家族树`, response.data);
|
|
return true;
|
|
} else if (response.status === 401) {
|
|
this.log('⚠️', '获取家族树列表', 'SKIP', '需要登录(权限保护正常)', response.data);
|
|
return false;
|
|
} else {
|
|
this.log('❌', '获取家族树列表', 'FAIL', `状态码: ${response.status}`, response.data);
|
|
return false;
|
|
}
|
|
} catch (error) {
|
|
this.log('❌', '获取家族树列表', 'ERROR', error.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async testCreateTree() {
|
|
try {
|
|
const response = await this.request('/api/trees', {
|
|
method: 'POST',
|
|
body: {
|
|
name: '测试家族树',
|
|
description: 'API 自动化测试创建'
|
|
}
|
|
});
|
|
|
|
if (response.status === 201) {
|
|
this.treeId = response.data.tree.id;
|
|
this.log('✅', '创建家族树', 'PASS', `成功创建家族树: ${this.treeId}`, response.data);
|
|
return true;
|
|
} else if (response.status === 401) {
|
|
this.log('⚠️', '创建家族树', 'SKIP', '需要登录(权限保护正常)', response.data);
|
|
return false;
|
|
} else {
|
|
this.log('❌', '创建家族树', 'FAIL', `状态码: ${response.status}`, response.data);
|
|
return false;
|
|
}
|
|
} catch (error) {
|
|
this.log('❌', '创建家族树', 'ERROR', error.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async testGetMembers() {
|
|
if (!this.treeId) {
|
|
this.log('⚠️', '获取成员列表', 'SKIP', '没有可用的家族树 ID');
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const response = await this.request(`/api/trees/${this.treeId}/members`, {
|
|
method: 'GET'
|
|
});
|
|
|
|
if (response.status === 200) {
|
|
this.log('✅', '获取成员列表', 'PASS', `成功获取 ${response.data.members?.length || 0} 个成员`, response.data);
|
|
return true;
|
|
} else if (response.status === 401 || response.status === 403) {
|
|
this.log('⚠️', '获取成员列表', 'SKIP', '需要权限', response.data);
|
|
return false;
|
|
} else {
|
|
this.log('❌', '获取成员列表', 'FAIL', `状态码: ${response.status}`, response.data);
|
|
return false;
|
|
}
|
|
} catch (error) {
|
|
this.log('❌', '获取成员列表', 'ERROR', error.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async testCreateMember() {
|
|
if (!this.treeId) {
|
|
this.log('⚠️', '创建成员', 'SKIP', '没有可用的家族树 ID');
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const response = await this.request(`/api/trees/${this.treeId}/members`, {
|
|
method: 'POST',
|
|
body: {
|
|
surname: '李',
|
|
givenName: '明',
|
|
fullName: '李明',
|
|
gender: 'MALE',
|
|
generation: 1,
|
|
birthDate: '1990-01-01'
|
|
}
|
|
});
|
|
|
|
if (response.status === 201) {
|
|
this.memberId = response.data.member.id;
|
|
this.log('✅', '创建成员', 'PASS', `成功创建成员: ${response.data.member.fullName}`, response.data);
|
|
return true;
|
|
} else if (response.status === 401 || response.status === 403) {
|
|
this.log('⚠️', '创建成员', 'SKIP', '需要权限', response.data);
|
|
return false;
|
|
} else {
|
|
this.log('❌', '创建成员', 'FAIL', `状态码: ${response.status}`, response.data);
|
|
return false;
|
|
}
|
|
} catch (error) {
|
|
this.log('❌', '创建成员', 'ERROR', error.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async testUnauthorizedAccess() {
|
|
// 清除 cookies 测试未授权访问
|
|
const savedCookies = { ...this.cookies };
|
|
this.cookies = {};
|
|
|
|
try {
|
|
const response = await this.request('/api/trees', {
|
|
method: 'GET'
|
|
});
|
|
|
|
if (response.status === 401) {
|
|
this.log('✅', '权限验证', 'PASS', '未登录正确返回 401', response.data);
|
|
this.cookies = savedCookies;
|
|
return true;
|
|
} else {
|
|
this.log('❌', '权限验证', 'FAIL', `应该返回 401,实际: ${response.status}`, response.data);
|
|
this.cookies = savedCookies;
|
|
return false;
|
|
}
|
|
} catch (error) {
|
|
this.log('❌', '权限验证', 'ERROR', error.message);
|
|
this.cookies = savedCookies;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
printSummary() {
|
|
console.log('\n\n' + '='.repeat(60));
|
|
console.log('📊 测试总结');
|
|
console.log('='.repeat(60));
|
|
|
|
const passed = this.results.filter(r => r.status === 'PASS').length;
|
|
const failed = this.results.filter(r => r.status === 'FAIL').length;
|
|
const errors = this.results.filter(r => r.status === 'ERROR').length;
|
|
const skipped = this.results.filter(r => r.status === 'SKIP').length;
|
|
const total = this.results.length;
|
|
|
|
console.log(`\n总计: ${total} 个测试`);
|
|
console.log(`✅ 通过: ${passed}`);
|
|
console.log(`❌ 失败: ${failed}`);
|
|
console.log(`⚠️ 跳过: ${skipped}`);
|
|
console.log(`💥 错误: ${errors}`);
|
|
|
|
const successRate = total > 0 ? ((passed / (total - skipped)) * 100).toFixed(1) : 0;
|
|
console.log(`\n成功率: ${successRate}%`);
|
|
|
|
if (failed === 0 && errors === 0) {
|
|
console.log('\n🎉 所有测试通过!');
|
|
} else {
|
|
console.log('\n⚠️ 存在失败的测试,请检查上面的详细信息');
|
|
}
|
|
|
|
console.log('='.repeat(60) + '\n');
|
|
}
|
|
|
|
async runAllTests() {
|
|
console.log('🚀 开始全面 API 测试...');
|
|
console.log('目标: http://localhost:3000');
|
|
console.log('='.repeat(60));
|
|
|
|
// 1. 测试注册
|
|
await this.testRegister();
|
|
|
|
// 2. 测试权限验证(未登录)
|
|
await this.testUnauthorizedAccess();
|
|
|
|
// 3. 获取 CSRF Token
|
|
await this.testGetCSRF();
|
|
|
|
// 4. 测试登录
|
|
await this.testLogin();
|
|
|
|
// 5. 测试获取家族树列表
|
|
await this.testGetTrees();
|
|
|
|
// 6. 测试创建家族树
|
|
await this.testCreateTree();
|
|
|
|
// 7. 测试获取成员列表
|
|
await this.testGetMembers();
|
|
|
|
// 8. 测试创建成员
|
|
await this.testCreateMember();
|
|
|
|
// 9. 再次获取成员列表(验证创建成功)
|
|
if (this.memberId) {
|
|
await this.testGetMembers();
|
|
}
|
|
|
|
// 打印总结
|
|
this.printSummary();
|
|
}
|
|
}
|
|
|
|
// 运行测试
|
|
const tester = new APITester('http://localhost:3000');
|
|
tester.runAllTests().catch(console.error);
|