| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560 |
- /**
- * CFC API Integration Test Runner v3
- * 环境: http://cfc.iwintrue.com:80
- * 覆盖: NEW-01/02/03修复验证 + ISSUE-001/002/003回归 + 新增API测试
- *
- * 运行: node tests/run-api-tests-v3.js
- */
- const BASE = 'http://cfc.iwintrue.com:80';
- const ADMIN = { phone: '13800138000', userId: 7, role: 'admin', name: '管理员' };
- const PARENT = { phone: '13701366188', userId: 8, familyId: 2, role: 'parent', name: '家长' };
- let adminToken = null;
- let testResults = [];
- let testStartTime = Date.now();
- function log(msg) { console.log(`[${new Date().toISOString()}] ${msg}`); }
- function pass(name, detail = '') {
- testResults.push({ name, result: 'PASS', detail, time: Date.now() });
- console.log(` ✅ PASS | ${name}${detail ? ' | ' + detail : ''}`);
- }
- function fail(name, err, detail = '') {
- testResults.push({ name, result: 'FAIL', detail: detail || err.message, time: Date.now() });
- console.log(` ❌ FAIL | ${name} | ${err.message}${detail ? ' | ' + detail : ''}`);
- }
- function skip(name, reason) {
- testResults.push({ name, result: 'SKIP', detail: reason, time: Date.now() });
- console.log(` ⏭️ SKIP | ${name} | ${reason}`);
- }
- async function api(path, body, headers = {}) {
- const url = `${BASE}${path}`;
- const opts = {
- method: 'POST',
- headers: { 'Content-Type': 'application/json', ...headers },
- body: body ? JSON.stringify(body) : undefined,
- };
- try {
- const resp = await fetch(url, opts);
- const text = await resp.text();
- let data;
- try { data = JSON.parse(text); } catch { data = null; }
- return { status: resp.status, data, ok: resp.ok, raw: text.substring(0, 300) };
- } catch (err) {
- throw new Error(`网络错误: ${err.message}`);
- }
- }
- async function adminApi(path, body) {
- if (!adminToken) throw new Error('无admin token');
- return api(path, body, { Authorization: `Bearer ${adminToken}` });
- }
- async function publicApi(path, body) {
- return api(path, body, {});
- }
- // ─────────────────────────────────────────
- // 1. 登录
- // ─────────────────────────────────────────
- async function login() {
- log('=== 1. 登录测试 ===');
- try {
- // 发送验证码(skip-captcha 已启用,任意验证码均可)
- await api('/api/admin-auth/send-code', { phone: ADMIN.phone });
- // 登录获取 token
- const resp = await api('/api/admin-auth/login', { phone: ADMIN.phone, code: '123456' });
- if (resp.data?.data?.token) {
- adminToken = resp.data.data.token;
- pass('Admin登录', `userId=${resp.data.data.adminId || ADMIN.userId}`);
- } else {
- fail('Admin登录', new Error('未获取到token'), `raw: ${resp.raw}`);
- }
- } catch (err) {
- fail('Admin登录', err);
- }
- skip('Parent登录(手机验证码)', '测试环境无法发短信,parent端需手动测试');
- }
- // ─────────────────────────────────────────
- // 2. 公共端点(无需认证)
- // ─────────────────────────────────────────
- async function testPublicEndpoints() {
- log('=== 2. 公共端点测试 ===');
- // 文章列表
- try {
- const resp = await publicApi('/api/articles/list', { page: 1, size: 5 });
- if (resp.ok || resp.data?.code === 200) {
- pass('文章列表(公开)', 'ok');
- } else {
- fail('文章列表(公开)', new Error(`code=${resp.data?.code}`));
- }
- } catch (err) { fail('文章列表(公开)', err); }
- // 活动列表
- try {
- const resp = await publicApi('/api/activity/list', { page: 1, size: 5 });
- if (resp.ok || resp.data?.code === 200) {
- pass('活动列表(公开)', 'ok');
- } else {
- fail('活动列表(公开)', new Error(`status=${resp.status}`));
- }
- } catch (err) { fail('活动列表(公开)', err); }
- }
- // ─────────────────────────────────────────
- // 3. NEW-01 验证:能量规则创建
- // 修复: AdminEnergyRuleController.create() 改用 Map<String,Object>
- // ─────────────────────────────────────────
- async function testNEW01_EnergyRuleCreate() {
- log('=== 3. NEW-01 验证:能量规则创建 ===');
- if (!adminToken) { skip('NEW-01 能量规则创建', '无admin token'); return; }
- // 先查看能量规则列表(确保维度存在)
- let dimensions = [];
- try {
- const resp = await adminApi('/api/admin/energy-rule/dimensions', {});
- if (resp.ok || resp.data?.code === 200) {
- dimensions = resp.data?.data || [];
- pass('能量维度列表', `count=${dimensions.length}`);
- } else {
- fail('能量维度列表', new Error(`code=${resp.data?.code}`));
- }
- } catch (err) { fail('能量维度列表', err); return; }
- // 创建能量规则(使用正确的 Map 参数格式)
- let ruleId = null;
- try {
- const resp = await adminApi('/api/admin/energy-rule/create', {
- ruleName: '自动化测试规则_NEW01_' + Date.now(),
- dimensionCode: 'body',
- eventType: 'checkin',
- energyValue: 5,
- status: 1,
- action: 'increase',
- priority: 0,
- });
- if (resp.ok || resp.data?.code === 200) {
- // 插入后 EnergyRule.id 会回填到 data
- ruleId = resp.data?.data?.id || resp.data?.id;
- pass('NEW-01 创建能量规则', `id=${ruleId} ✅ NEW-01已修复`);
- } else {
- fail('NEW-01 创建能量规则', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`));
- }
- } catch (err) { fail('NEW-01 创建能量规则', err); }
- if (!ruleId) return;
- // 切换状态
- try {
- const resp = await adminApi('/api/admin/energy-rule/toggle', { id: ruleId });
- if (resp.ok || resp.data?.code === 200) {
- pass('能量规则切换状态', `id=${ruleId}`);
- } else {
- fail('能量规则切换状态', new Error(`code=${resp.data?.code}`));
- }
- } catch (err) { fail('能量规则切换状态', err); }
- }
- // ─────────────────────────────────────────
- // 4. NEW-02 验证:SKU列表
- // 修复: ProductSkuController.listByProduct 改用 Map + null检查
- // ─────────────────────────────────────────
- async function testNEW02_SkuList() {
- log('=== 4. NEW-02 验证:SKU列表 ===');
- if (!adminToken) { skip('NEW-02 SKU列表', '无admin token'); return; }
- // productId=1 在之前测试中应该是已存在的商品
- try {
- const resp = await adminApi('/api/admin/product/sku/list', { productId: 1 });
- if (resp.ok || resp.data?.code === 200) {
- const skus = resp.data?.data || [];
- pass('NEW-02 SKU列表', `count=${skus.length} ✅ NEW-02已修复`);
- } else if (resp.data?.code === 500) {
- fail('NEW-02 SKU列表', new Error('500 Internal Server Error'), '⚠️ NEW-02可能回归');
- } else {
- fail('NEW-02 SKU列表', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`));
- }
- } catch (err) { fail('NEW-02 SKU列表', err); }
- }
- // ─────────────────────────────────────────
- // 5. NEW-03 验证:活动发布
- // 修复: ActivityController.publish() 改用 Map + try-catch
- // ─────────────────────────────────────────
- async function testNEW03_ActivityPublish() {
- log('=== 5. NEW-03 验证:活动发布 ===');
- if (!adminToken) { skip('NEW-03 活动发布', '无admin token'); return; }
- // 创建活动
- let activityId = null;
- try {
- const resp = await adminApi('/api/activity/create', {
- title: '自动化E2E测试活动_NEW03_' + Date.now(),
- description: 'NEW-03修复验证测试',
- dimensionCode: 'body',
- startTime: new Date(Date.now() + 86400000 * 7).toISOString().replace('T', ' ').split('.')[0],
- endTime: new Date(Date.now() + 86400000 * 8).toISOString().replace('T', ' ').split('.')[0],
- location: '线上直播',
- maxParticipants: 30,
- });
- if (resp.ok || resp.data?.code === 200) {
- activityId = resp.data?.data?.id || resp.data?.id;
- pass('创建测试活动', `id=${activityId}`);
- } else {
- fail('创建测试活动', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`));
- }
- } catch (err) { fail('创建测试活动', err); }
- if (!activityId) return;
- // 发布活动(NEW-03 核心测试)
- try {
- const resp = await adminApi('/api/activity/publish', { id: activityId });
- if (resp.ok || resp.data?.code === 200) {
- pass('NEW-03 发布活动', `id=${activityId} ✅ NEW-03已修复`);
- } else if (resp.data?.code === 500) {
- fail('NEW-03 发布活动', new Error('500 Internal Server Error'), '⚠️ NEW-03可能回归');
- } else {
- fail('NEW-03 发布活动', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`));
- }
- } catch (err) { fail('NEW-03 发布活动', err); }
- }
- // ─────────────────────────────────────────
- // 6. ISSUE-001 验证:供应商下架商品
- // ─────────────────────────────────────────
- async function testISSUE001() {
- log('=== 6. ISSUE-001 验证:供应商下架商品 ===');
- if (!adminToken) { skip('ISSUE-001 供应商下架', '无admin token'); return; }
- // 1) 审核供应商 81069(如尚未批准)
- try {
- const resp = await adminApi('/api/admin/vendor/review', {
- userId: 81069,
- action: 'approve',
- reason: '自动化测试',
- });
- if (resp.ok || resp.data?.code === 200) {
- pass('审核供应商81069', 'approved');
- } else {
- fail('审核供应商81069', new Error(`code=${resp.data?.code}`));
- }
- } catch (err) { fail('审核供应商81069', err); }
- // 2) 审核商品(如尚未批准)
- try {
- const resp = await adminApi('/api/admin/product/review', {
- productId: 7,
- action: 'approve',
- reason: '自动化测试',
- });
- if (resp.ok || resp.data?.code === 200) {
- pass('审核商品7', 'approved');
- } else {
- fail('审核商品7', new Error(`code=${resp.data?.code}`));
- }
- } catch (err) { fail('审核商品7', err); }
- // 3) 上架商品
- try {
- const resp = await adminApi('/api/admin/product/shelve', { productId: 7, shelve: true });
- if (resp.ok || resp.data?.code === 200) {
- pass('上架商品7', 'on_shelf');
- } else {
- fail('上架商品7', new Error(`code=${resp.data?.code}`));
- }
- } catch (err) { fail('上架商品7', err); }
- // 4) 供应商下架商品(核心验证)
- try {
- const resp = await api('/api/product/shelve', { productId: 7, action: 'unshelve' }, { 'X-User-Id': '81069' });
- if (resp.ok || resp.data?.code === 200) {
- pass('ISSUE-001 供应商下架商品', 'unshelved ✅ ISSUE-001已修复');
- } else if (resp.data?.message?.includes('未通过审核')) {
- fail('ISSUE-001 供应商下架商品', new Error('商品未通过审核/状态异常'), '需检查商品状态');
- } else {
- fail('ISSUE-001 供应商下架商品', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`));
- }
- } catch (err) { fail('ISSUE-001 供应商下架商品', err); }
- }
- // ─────────────────────────────────────────
- // 7. ISSUE-002/003 回归验证
- // ─────────────────────────────────────────
- async function testIssue002_003() {
- log('=== 7. ISSUE-002/003 回归验证 ===');
- if (!adminToken) { skip('ISSUE-002/003', '无admin token'); return; }
- // ISSUE-003: 创建用户
- const phone = '139' + String(Math.floor(Math.random() * 1e9)).padStart(9, '0');
- let newUserId = null;
- try {
- const resp = await adminApi('/api/admin/users/create', {
- phone: phone,
- password: 'Test123456',
- name: '自动化测试用户_' + Date.now(),
- role: 'parent',
- });
- if (resp.ok || resp.data?.code === 200) {
- newUserId = resp.data?.data?.id || resp.data?.id;
- pass('ISSUE-003 创建用户', `userId=${newUserId} ✅ ISSUE-003稳定`);
- } else if (resp.data?.code === 500) {
- fail('ISSUE-003 创建用户', new Error('500 - ISSUE-003回归'), '⚠️ 需紧急修复');
- } else {
- fail('ISSUE-003 创建用户', new Error(`code=${resp.data?.code}`));
- }
- } catch (err) { fail('ISSUE-003 创建用户', err); }
- // ISSUE-002: 文章发布(检查 articleId=13 是否仍可发布)
- try {
- const resp = await adminApi('/api/article/13/publish', {});
- if (resp.ok || resp.data?.code === 200) {
- pass('ISSUE-002 文章发布', 'articleId=13 ✅ ISSUE-002稳定');
- } else {
- fail('ISSUE-002 文章发布', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`));
- }
- } catch (err) { fail('ISSUE-002 文章发布', err); }
- }
- // ─────────────────────────────────────────
- // 8. 新增API测试(来自 recent commits)
- // dfcc285: 心愿兑换积分检查和扣除
- // 3e44f18: grantEnergy + deductForWish 接口
- // 8212e25: getRecordsByChild 成长记录
- // ─────────────────────────────────────────
- async function testNewAPIs() {
- log('=== 8. 新增API测试(recent commits)===');
- if (!adminToken) { skip('新增API测试', '无admin token'); return; }
- // 8a. EnergyService.grantEnergy + deductForWish
- // POST /api/energy/grantEnergy
- try {
- const resp = await adminApi('/api/energy/grantEnergy', {
- childId: 1,
- taskId: 1,
- amount: 10,
- dimensionCode: 'body',
- });
- if (resp.ok || resp.data?.code === 200) {
- pass('energy.grantEnergy', 'ok');
- } else {
- fail('energy.grantEnergy', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`));
- }
- } catch (err) { fail('energy.grantEnergy', err); }
- // POST /api/energy/deductForWish
- try {
- const resp = await adminApi('/api/energy/deductForWish', {
- childId: 1,
- wishId: 1,
- amount: 5,
- });
- if (resp.ok || resp.data?.code === 200) {
- pass('energy.deductForWish', 'ok');
- } else {
- fail('energy.deductForWish', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`));
- }
- } catch (err) { fail('energy.deductForWish', err); }
- // 8b. PointsService.checkAndGrantReward + deductForWish (dfcc285)
- // POST /api/points/checkAndGrantReward
- try {
- const resp = await adminApi('/api/points/checkAndGrantReward', {
- childId: 1,
- amount: 5,
- });
- // 可能返回 -1(余额不足)或成功
- if (resp.ok || resp.data?.code === 200 || resp.data?.data === -1) {
- pass('points.checkAndGrantReward', `result=${resp.data?.data}`);
- } else {
- fail('points.checkAndGrantReward', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`));
- }
- } catch (err) { fail('points.checkAndGrantReward', err); }
- // POST /api/points/deductForWish
- try {
- const resp = await adminApi('/api/points/deductForWish', {
- childId: 1,
- amount: 5,
- });
- if (resp.ok || resp.data?.code === 200) {
- pass('points.deductForWish', `result=${resp.data?.data}`);
- } else {
- fail('points.deductForWish', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`));
- }
- } catch (err) { fail('points.deductForWish', err); }
- // 8c. GrowthRecord - getRecordsByChild (8212e25)
- // GET /api/growth/record/child/{childId} 或 POST
- try {
- const resp = await adminApi('/api/growth/record/child/1', {});
- if (resp.ok || resp.data?.code === 200) {
- const records = resp.data?.data || [];
- pass('growth.getRecordsByChild', `count=${Array.isArray(records) ? records.length : 0}`);
- } else {
- fail('growth.getRecordsByChild', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`));
- }
- } catch (err) { fail('growth.getRecordsByChild', err); }
- // 8d. Activity end
- try {
- const resp = await adminApi('/api/activity/end', { id: 18 });
- if (resp.ok || resp.data?.code === 200 || resp.data?.message?.includes('不存在')) {
- pass('activity.end', 'ok');
- } else {
- fail('activity.end', new Error(`code=${resp.data?.code} msg=${resp.data?.message || ''}`));
- }
- } catch (err) { fail('activity.end', err); }
- }
- // ─────────────────────────────────────────
- // 9. 能量规则维度查询
- // ─────────────────────────────────────────
- async function testEnergyRuleDimensions() {
- log('=== 9. 能量规则维度查询 ===');
- if (!adminToken) { skip('能量规则维度查询', '无admin token'); return; }
- try {
- const resp = await adminApi('/api/admin/energy-rule/dimensions', {});
- if (resp.ok || resp.data?.code === 200) {
- const dims = resp.data?.data || [];
- pass('能量规则维度列表', `count=${dims.length}`);
- dims.forEach(d => {
- console.log(` - ${d.dimensionCode || d.code}: ${d.name || d.dimensionName || JSON.stringify(d)}`);
- });
- } else {
- fail('能量规则维度列表', new Error(`code=${resp.data?.code}`));
- }
- } catch (err) { fail('能量规则维度列表', err); }
- }
- // ─────────────────────────────────────────
- // 10. 后台管理核心接口
- // ─────────────────────────────────────────
- async function testAdminCore() {
- log('=== 10. 后台管理核心接口 ===');
- if (!adminToken) { skip('后台管理核心接口', '无admin token'); return; }
- // 用户列表
- try {
- const resp = await adminApi('/api/admin/users', { page: 1, size: 10 });
- if (resp.ok || resp.data?.code === 200) {
- pass('用户列表', 'ok');
- } else {
- fail('用户列表', new Error(`code=${resp.data?.code}`));
- }
- } catch (err) { fail('用户列表', err); }
- // 供应商列表
- try {
- const resp = await adminApi('/api/admin/vendor/list', {});
- if (resp.ok || resp.data?.code === 200) {
- pass('供应商列表', 'ok');
- } else {
- fail('供应商列表', new Error(`code=${resp.data?.code}`));
- }
- } catch (err) { fail('供应商列表', err); }
- // 规划师申请列表
- try {
- const resp = await adminApi('/api/admin/guide/applications/pending', {});
- if (resp.ok || resp.data?.code === 200) {
- const apps = resp.data?.data || [];
- pass('规划师申请列表', `pending=${apps.length}`);
- } else {
- fail('规划师申请列表', new Error(`code=${resp.data?.code}`));
- }
- } catch (err) { fail('规划师申请列表', err); }
- }
- // ─────────────────────────────────────────
- // 报告生成
- // ─────────────────────────────────────────
- async function generateReport() {
- const duration = Date.now() - testStartTime;
- const passed = testResults.filter(r => r.result === 'PASS').length;
- const failed = testResults.filter(r => r.result === 'FAIL').length;
- const skipped = testResults.filter(r => r.result === 'SKIP').length;
- const total = testResults.length;
- console.log('\n' + '='.repeat(60));
- console.log(` API 测试报告 v3 — ${new Date().toLocaleString()}`);
- console.log(` 耗时: ${Math.round(duration / 1000)}s`);
- console.log(` 结果: ${passed} PASS / ${failed} FAIL / ${skipped} SKIP / ${total} TOTAL`);
- console.log('='.repeat(60));
- if (failed > 0) {
- console.log('\n❌ FAIL 明细:');
- testResults.filter(r => r.result === 'FAIL').forEach(r => {
- console.log(` - ${r.name}: ${r.detail}`);
- });
- }
- const report = {
- version: 'v3',
- timestamp: new Date().toISOString(),
- duration_ms: duration,
- summary: { total, passed, failed, skipped },
- results: testResults,
- };
- const fs = require('fs');
- const path = require('path');
- const reportPath = path.join(__dirname, 'TEST-RESULTS-v3.md');
- let md = `# API 测试报告 v3 — ${new Date().toLocaleString()}\n\n`;
- md += `| 结果 | 数量 |\n|------|------|\n`;
- md += `| ✅ PASS | ${passed} |\n| ❌ FAIL | ${failed} |\n| ⏭️ SKIP | ${skipped} |\n| 总计 | ${total} |\n\n`;
- md += `## FAIL 明细\n\n`;
- testResults.filter(r => r.result === 'FAIL').forEach(r => {
- md += `- **${r.name}**: ${r.detail}\n`;
- });
- md += `\n## 完整结果\n\n`;
- md += `| 用例 | 结果 | 详情 |\n|------|------|------|\n`;
- testResults.forEach(r => {
- md += `| ${r.name} | ${r.result} | ${r.detail} |\n`;
- });
- fs.writeFileSync(reportPath, md, 'utf8');
- console.log(`\n报告已保存: ${reportPath}`);
- }
- // ─────────────────────────────────────────
- // 主流程
- // ─────────────────────────────────────────
- async function main() {
- log('CFC API 测试 Runner v3 开始');
- log(`目标环境: ${BASE}`);
- await login();
- await testPublicEndpoints();
- await testNEW01_EnergyRuleCreate();
- await testNEW02_SkuList();
- await testNEW03_ActivityPublish();
- await testISSUE001();
- await testIssue002_003();
- await testEnergyRuleDimensions();
- await testAdminCore();
- await testNewAPIs();
- await generateReport();
- }
- main().catch(console.error);
|