import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import crypto from 'crypto'; import os from 'os'; import path from 'path'; import type { Server } from 'http'; // 关闭 DNS 解析校验(同 feature-review,避免测试依赖真实网络) process.env.SSRF_DNS_CHECK = 'off'; // 使用独立临时库,避免污染/耦合真实 data/ai-review.db process.env.DB_PATH = path.join(os.tmpdir(), `ai-review-api-${Date.now()}-${Math.random().toString(36).slice(2)}.db`); let server: Server; let token = ''; let projectId = ''; let standardId = ''; let entryId = ''; function headers(extra: Record = {}): Record { return { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), ...extra }; } const BASE = 'http://localhost:18902'; async function api(method: string, path: string, body?: any): Promise<{ status: number; data: any }> { const res = await fetch(`${BASE}${path}`, { method, headers: headers(), body: body ? JSON.stringify(body) : undefined, }); const text = await res.text(); let data: any; try { data = JSON.parse(text); } catch { data = text; } return { status: res.status, data }; } beforeAll(async () => { // Import app after env is configured process.env.ADMIN_TEST_TOKEN = 'true'; // 启用 force-review 测试端点 const mod = await import('../index'); const app = mod.app; const { config } = await import('../config'); server = app.listen(18902); // Wait for server ready await fetch(`${BASE}/api/health`); }); afterAll(async () => { server?.close(); }); describe('Auth API', () => { it('TC-AUTH-06: should reject without token', async () => { const { status, data } = await api('GET', '/api/projects'); expect(status).toBe(401); expect(data.error).toBe('未登录'); }); it('TC-AUTH-07: should reject invalid token', async () => { token = 'xxx-invalid'; const { status, data } = await api('GET', '/api/projects'); expect(status).toBe(401); expect(data.error).toBe('登录已过期'); token = ''; }); it('TC-AUTH-02: should reject wrong password', async () => { const { status, data } = await api('POST', '/api/auth/login', { password: 'wrong' }); expect(status).toBe(401); expect(data.error).toBe('密码错误'); }); it('TC-AUTH-01: should login with correct password', async () => { const { config } = await import('../config'); const { status, data } = await api('POST', '/api/auth/login', { password: config.authPassword }); expect(status).toBe(200); expect(data.token).toBeTruthy(); token = data.token; }); it('TC-AUTH-10: /api/health should work without auth', async () => { const oldToken = token; token = ''; const { status, data } = await api('GET', '/api/health'); expect(status).toBe(200); expect(data.status).toBe('ok'); token = oldToken; }); it('TC-AUTH-08: should access API with valid token', async () => { const { status } = await api('GET', '/api/projects'); expect(status).toBe(200); }); }); describe('Projects API', () => { it('TC-PROJ-05: should list empty projects', async () => { const { status, data } = await api('GET', '/api/projects'); expect(status).toBe(200); expect(Array.isArray(data)).toBe(true); }); it('TC-PROJ-02: should reject empty name', async () => { const { status, data } = await api('POST', '/api/projects', { name: '' }); expect(status).toBe(400); expect(data.error).toBe('项目名称为必填项'); }); it('TC-PROJ-02b: should reject missing track', async () => { const { status, data } = await api('POST', '/api/projects', { name: '无赛道项目' }); expect(status).toBe(400); expect(data.error).toContain('赛道'); }); it('TC-PROJ-01: should create project', async () => { const { status, data } = await api('POST', '/api/projects', { name: '测试项目', description: '集成测试', track: '赛道二' }); expect(status).toBe(200); expect(data.id).toBeTruthy(); expect(data.name).toBe('测试项目'); projectId = data.id; }); it('TC-PROJ-03: should trim name', async () => { const { status, data } = await api('POST', '/api/projects', { name: ' 空格项目 ', track: '赛道二' }); expect(status).toBe(200); expect(data.name).toBe('空格项目'); }); it('TC-PROJ-04: should list projects with stats', async () => { const { status, data } = await api('GET', '/api/projects'); expect(status).toBe(200); expect(data.length).toBeGreaterThanOrEqual(2); expect(data[0].total).toBeDefined(); expect(data[0].reviewed).toBeDefined(); }); it('TC-PROJ-07: should 404 for non-existent project', async () => { const { status, data } = await api('GET', '/api/projects/non-existent'); expect(status).toBe(404); expect(data.error).toBe('项目不存在'); }); it('TC-PROJ-06: should get project detail', async () => { const { status, data } = await api('GET', `/api/projects/${projectId}`); expect(status).toBe(200); expect(data.id).toBe(projectId); expect(data.total).toBeDefined(); expect(data.standards).toBeDefined(); }); it('TC-PROJ-08: should update project', async () => { const { status, data } = await api('PUT', `/api/projects/${projectId}`, { name: '更新后项目名' }); expect(status).toBe(200); expect(data.name).toBe('更新后项目名'); }); it('TC-PROJ-09: should partially update project', async () => { const { status, data } = await api('PUT', `/api/projects/${projectId}`, { description: '仅改描述' }); expect(status).toBe(200); expect(data.description).toBe('仅改描述'); }); }); describe('Standards API', () => { const validContent = '## 代码质量(30分)\n代码整洁度\n## 架构设计(40分)\n模块化程度\n## 测试覆盖(30分)\n自动化测试'; it('TC-STD-02: should reject invalid format', async () => { const { status, data } = await api('POST', `/api/projects/${projectId}/standards`, { name: '无效格式', content: '普通文本', }); expect(status).toBe(400); expect(data.error).toContain('格式异常'); }); it('TC-STD-03: should reject total > max', async () => { const { status, data } = await api('POST', `/api/projects/${projectId}/standards`, { name: '超分', content: '## 维度一(100分)\nx\n## 维度二(80分)\nx', }); expect(status).toBe(400); expect(data.error).toContain('超过'); }); it('TC-STD-04: should reject empty name', async () => { const { status, data } = await api('POST', `/api/projects/${projectId}/standards`, { name: '', content: validContent, }); expect(status).toBe(400); expect(data.error).toBe('标准名称为必填项'); }); it('TC-STD-01: should create standard', async () => { const { status, data } = await api('POST', `/api/projects/${projectId}/standards`, { name: 'L2标准', content: validContent, category_tag: '算法', }); expect(status).toBe(200); expect(data.id).toBeTruthy(); expect(data.dimensions).toHaveLength(3); expect(data.category_tag).toBe('算法'); standardId = data.id; }); it('TC-STD-MAX: should create standard with custom max_score', async () => { const { status, data } = await api('POST', `/api/projects/${projectId}/standards`, { name: '限120分标准', content: '## a(60分)\n## b(60分)', max_score: '120', }); expect(status).toBe(200); expect(data.max_score).toBe(120); // Cleanup await api('DELETE', `/api/projects/${projectId}/standards/${data.id}`); }); it('TC-STD-MAX-OVER: should reject total over custom max_score', async () => { const { status, data } = await api('POST', `/api/projects/${projectId}/standards`, { name: '超自定义上限', content: '## a(70分)\n## b(60分)', max_score: '100', }); expect(status).toBe(400); expect(data.error).toContain('超过'); }); it('TC-STD-06: should create standard with category_tag', async () => { const { status, data } = await api('POST', `/api/projects/${projectId}/standards`, { name: '默认标准', content: validContent, }); expect(status).toBe(200); expect(data.category_tag).toBe(''); }); it('TC-STD-07: should list standards', async () => { const { status, data } = await api('GET', `/api/projects/${projectId}/standards`); expect(status).toBe(200); expect(data.length).toBeGreaterThanOrEqual(2); expect(data[0].dimensions).toBeDefined(); }); it('TC-STD-10: should update standard', async () => { const { status, data } = await api('PUT', `/api/projects/${projectId}/standards/${standardId}`, { name: '更新后标准', }); expect(status).toBe(200); expect(data.name).toBe('更新后标准'); }); }); describe('Entries API', () => { // 外部 file:// 路径:被启动评审时克隆秒失败(不触发真实网络),保持测试确定性 const repoUrl = `file://C:\\test-repo-${Date.now()}`; it('TC-ENT-04: should fallback to default standard when category tag does not match', async () => { const { status, data } = await api('POST', `/api/projects/${projectId}/entries`, { title: '使用默认标准', repo_url: 'https://example.com/none.git', category_tag: '不存在', }); expect(status).toBe(200); expect(data.standard_snapshot).toBeTruthy(); }); it('TC-ENT-02: should reject empty title', async () => { const { status, data } = await api('POST', `/api/projects/${projectId}/entries`, { repo_url: repoUrl, }); expect(status).toBe(400); expect(data.error).toBe('标题为必填项'); }); it('TC-ENT-03: should reject empty repo_url', async () => { const { status, data } = await api('POST', `/api/projects/${projectId}/entries`, { title: '无仓库', }); expect(status).toBe(400); expect(data.error).toBe('仓库地址为必填项'); }); it('TC-ENT-01: should create entry', async () => { const { status, data } = await api('POST', `/api/projects/${projectId}/entries`, { title: '选手A', repo_url: repoUrl, difficulty: '★★★', participant: '张三', category_tag: '算法', }); expect(status).toBe(200); expect(data.id).toBeTruthy(); expect(data.pass_line).toBe(60); expect(data.standard_snapshot).toBeTruthy(); expect(data.status).toBe('pending'); entryId = data.id; }); it('TC-ENT-20: should create entry with base_branch', async () => { const url = `https://example.com/base-branch-${Date.now()}.git`; const { status, data } = await api('POST', `/api/projects/${projectId}/entries`, { title: '赛道二选手', repo_url: url, base_branch: 'main', }); expect(status).toBe(200); expect(data.base_branch).toBe('main'); }); it('TC-ENT-21: base_branch defaults to empty', async () => { const url = `https://example.com/no-branch-${Date.now()}.git`; const { status, data } = await api('POST', `/api/projects/${projectId}/entries`, { title: '无分支', repo_url: url, }); expect(status).toBe(200); expect(data.base_branch).toBe(''); }); it('TC-ENT-06: pass line calculation', async () => { const url = `https://example.com/pass-line-${Date.now()}.git`; const { data } = await api('POST', `/api/projects/${projectId}/entries`, { title: '通过线验证', repo_url: url, }); // standard total = 30+40+30 = 100, pass_line = 100 * 60% = 60 expect(data.pass_line).toBe(60); }); it('TC-ENT-07: should list entries', async () => { const { status, data } = await api('GET', `/api/projects/${projectId}/entries`); expect(status).toBe(200); expect(data.items.length).toBeGreaterThan(0); expect(data.total).toBeGreaterThan(0); }); it('TC-ENT-09: should get entry detail', async () => { const { status, data } = await api('GET', `/api/projects/${projectId}/entries/${entryId}`); expect(status).toBe(200); expect(data.id).toBe(entryId); expect(data.dimensions).toBeDefined(); // 赛道必选后条目使用项目对应赛道的默认标准(赛道二模板 8 维),不再是无赛道时的 3 维 fallback expect(data.dimensions.length).toBe(8); expect(data.standard_snapshot).toBeTruthy(); }); it('TC-ENT-10: should 404 non-existent entry', async () => { const { status, data } = await api('GET', `/api/projects/${projectId}/entries/non-existent`); expect(status).toBe(404); expect(data.error).toBe('条目不存在'); }); it('TC-ENT-11: should update pending entry', async () => { const { status, data } = await api('PUT', `/api/projects/${projectId}/entries/${entryId}`, { title: '更新后选手A', }); expect(status).toBe(200); expect(data.title).toBe('更新后选手A'); }); it('TC-ENT-22: should update base_branch', async () => { const { status, data } = await api('PUT', `/api/projects/${projectId}/entries/${entryId}`, { base_branch: 'develop', }); expect(status).toBe(200); expect(data.base_branch).toBe('develop'); }); it('TC-ENT-23: should clear base_branch', async () => { const { status, data } = await api('PUT', `/api/projects/${projectId}/entries/${entryId}`, { base_branch: '', }); expect(status).toBe(200); expect(data.base_branch).toBe(''); }); it('TC-ENT-15: should batch import', async () => { const entries = [ { title: '批量A', repo_url: `https://batch-a-${Date.now()}.git` }, { title: '批量B', repo_url: `https://batch-b-${Date.now()}.git` }, ]; const { status, data } = await api('POST', `/api/projects/${projectId}/entries/batch`, { entries }); expect(status).toBe(200); expect(data.imported).toBe(2); expect(data.items).toHaveLength(2); }); it('TC-ENT-16: should report batch errors', async () => { const entries = [ { title: '', repo_url: 'https://x.git' }, { title: '好项', repo_url: `https://good-${Date.now()}.git` }, ]; const { data } = await api('POST', `/api/projects/${projectId}/entries/batch`, { entries }); expect(data.imported).toBe(1); expect(data.errors).toHaveLength(1); expect(data.errors[0].reason).toContain('标题为空'); }); it('TC-ENT-17: should reject empty batch array', async () => { const { status, data } = await api('POST', `/api/projects/${projectId}/entries/batch`, { entries: [] }); expect(status).toBe(400); expect(data.error).toBe('请提供条目列表'); }); it('TC-ENT-18: should start review', async () => { const { status, data } = await api('POST', `/api/projects/${projectId}/entries/${entryId}/start`); expect(status).toBe(200); expect(data.success).toBe(true); }); it('TC-ENT-19: should not start already started entry', async () => { const { status, data } = await api('POST', `/api/projects/${projectId}/entries/${entryId}/start`); expect(status).toBe(409); expect(data.error).toContain('不允许启动'); }); }); describe('Summary API', () => { it('TC-PROJ-13: should return summary', async () => { const { status, data } = await api('GET', `/api/projects/${projectId}/summary`); expect(status).toBe(200); expect(data.project).toBeDefined(); expect(data.totalEntries).toBeDefined(); expect(data.categories).toBeDefined(); }); }); describe('Pagination Limit Clamp(K6 回归)', () => { it('TC-LIMIT-01: limit 超上限 clamp 到 500,limit=0 回落默认 50,offset 负数归零', async () => { for (let i = 0; i < 2; i++) { const r = await api('POST', `/api/projects/${projectId}/entries`, { title: `lim-${i}`, repo_url: `file://C:\\lim-${i}-${Date.now()}` }); expect(r.status).toBe(200); } const big = await api('GET', `/api/projects/${projectId}/entries?limit=100000`); expect(big.status).toBe(200); expect(big.data.limit).toBe(500); const neg = await api('GET', `/api/projects/${projectId}/entries?offset=-5&limit=0`); expect(neg.status).toBe(200); expect(neg.data.offset).toBe(0); expect(neg.data.limit).toBe(50); }); }); describe('Cookie Auth(K7 回归)', () => { it('TC-AUTH-11: 登录写入 httpOnly cookie,/auth/me 与业务接口可用,logout 清除 cookie', async () => { const { config } = await import('../config'); const loginRes = await fetch(`${BASE}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: config.authPassword }), }); expect(loginRes.status).toBe(200); const setCookie = loginRes.headers.get('set-cookie') || ''; expect(setCookie).toContain('token='); expect(setCookie.toLowerCase()).toContain('httponly'); const cookie = setCookie.split(';')[0]; const meRes = await fetch(`${BASE}/api/auth/me`, { headers: { Cookie: cookie } }); expect(meRes.status).toBe(200); expect((await meRes.json()).role).toBe('admin'); const projRes = await fetch(`${BASE}/api/projects`, { headers: { Cookie: cookie } }); expect(projRes.status).toBe(200); const logoutRes = await fetch(`${BASE}/api/auth/logout`, { method: 'POST', headers: { Cookie: cookie } }); expect(logoutRes.status).toBe(200); expect((logoutRes.headers.get('set-cookie') || '').toLowerCase()).toMatch(/token=;/i); }); }); describe('Admin Correction: cap + L2 final_level(整合后回归)', () => { let pid = ''; let eid = ''; beforeAll(async () => { const proj = await api('POST', '/api/projects', { name: 'report-cap-test', track: 'L2考核' }); pid = proj.data.id; const list = await api('GET', `/api/projects/${pid}/standards`); for (const s of list.data) { await api('DELETE', `/api/projects/${pid}/standards/${s.id}`); } // L2 7维标准:功能30+设计10+测试10+AI协作15+技术选型15+代码+README10+业务场景10=100 await api('POST', `/api/projects/${pid}/standards`, { name: 'rc-std', category_tag: 'L2考核', content: '## 功能完整性(30分)\n## 设计文档(10分)\n## 测试用例与测试结果(10分)\n## AI协作过程记录(15分)\n## 技术选型与范式运用(15分)\n## 代码质量+README(10分)\n## 业务场景理解与需求分析(10分)', }); const entry = await api('POST', `/api/projects/${pid}/entries`, { title: 'cap-entry', repo_url: `file://C:\\cap-${Date.now()}`, selected_topic: 'self', }); eid = entry.data.id; }); afterAll(async () => { await api('DELETE', `/api/projects/${pid}?force=true`); }); it('TC-REPORT-01: L2 人工修正后 final_level 按合格/不合格重算(含功能完整性地板线)', async () => { const full = [ { name: '功能完整性', score: 30, maxScore: 30 }, { name: '设计文档', score: 10, maxScore: 10 }, { name: '测试用例与测试结果', score: 10, maxScore: 10 }, { name: 'AI协作过程记录', score: 15, maxScore: 15 }, { name: '技术选型与范式运用', score: 15, maxScore: 15 }, { name: '代码质量+README', score: 10, maxScore: 10 }, { name: '业务场景理解与需求分析', score: 10, maxScore: 10 }, ]; const fr = await api('PUT', `/api/projects/${pid}/entries/${eid}/force-review`, { dimensions: full }); expect(fr.status).toBe(200); const seed = await api('PUT', `/api/projects/${pid}/entries/${eid}/report`, { dimensions: full }); expect(seed.status).toBe(200); let detail = await api('GET', `/api/projects/${pid}/entries/${eid}`); expect(detail.data.final_level).toBe('合格'); // 总分100 ≥60 且 功能30 ≥15 // 功能完整性低于地板线15 → 即使总分高也判不合格 const lowFunc = full.map(d => d.name === '功能完整性' ? { ...d, score: 10 } : d); const fix = await api('PUT', `/api/projects/${pid}/entries/${eid}/report`, { dimensions: lowFunc }); expect(fix.status).toBe(200); detail = await api('GET', `/api/projects/${pid}/entries/${eid}`); expect(detail.data.final_level).toBe('不合格'); // 功能10 < 15 地板线 // 恢复 const fix2 = await api('PUT', `/api/projects/${pid}/entries/${eid}/report`, { dimensions: full }); expect(fix2.status).toBe(200); detail = await api('GET', `/api/projects/${pid}/entries/${eid}`); expect(detail.data.final_level).toBe('合格'); }); it('TC-REPORT-02: 人工修正应用 max_score_cap 后再扣迟交', async () => { const db = (await import('../db')).default; db.prepare('UPDATE entries SET max_score_cap = 30, late_days = 0 WHERE id = ?').run(eid); const full = [ { name: '功能完整性', score: 30, maxScore: 30 }, { name: '设计文档', score: 10, maxScore: 10 }, { name: '测试用例与测试结果', score: 10, maxScore: 10 }, { name: 'AI协作过程记录', score: 15, maxScore: 15 }, { name: '技术选型与范式运用', score: 15, maxScore: 15 }, { name: '代码质量+README', score: 10, maxScore: 10 }, { name: '业务场景理解与需求分析', score: 10, maxScore: 10 }, ]; const fix = await api('PUT', `/api/projects/${pid}/entries/${eid}/report`, { dimensions: full }); expect(fix.status).toBe(200); const detail = await api('GET', `/api/projects/${pid}/entries/${eid}`); expect(detail.data.raw_score).toBe(100); expect(detail.data.final_score).toBe(30); // min(100, cap30) - 0 db.prepare('UPDATE entries SET max_score_cap = 100 WHERE id = ?').run(eid); }); }); describe('Retry attempt + deliverables + delete guard(K2/§5.1 回归)', () => { let pid = ''; let eid = ''; beforeAll(async () => { const proj = await api('POST', '/api/projects', { name: `misc-${Date.now()}`, track: '赛道一' }); pid = proj.data.id; await api('POST', `/api/projects/${pid}/standards`, { name: 'm-std', content: '## 场景价值(8分)\n## 架构设计(5分)' }); const entry = await api('POST', `/api/projects/${pid}/entries`, { title: 'm-entry', repo_url: `file://C:\\misc-${Date.now()}` }); eid = entry.data.id; }); it('TC-ENT-RETRY: retry 使 attempt+1(K2 回归)', async () => { const db = (await import('../db')).default; db.prepare("UPDATE entries SET status = 'failed', attempt = 1 WHERE id = ?").run(eid); const r = await api('POST', `/api/projects/${pid}/entries/${eid}/retry`); expect(r.status).toBe(200); const detail = await api('GET', `/api/projects/${pid}/entries/${eid}`); expect(detail.data.attempt).toBe(2); expect(['queued', 'cloning', 'clone_fail']).toContain(detail.data.status); }); it('TC-DELIV-01: 成果物初始化/汇总/CSV 导出', async () => { const init = await api('PUT', `/api/projects/${pid}/entries/deliverables/init`); expect(init.status).toBe(200); expect(init.data.initialized).toBe(1); const sum = await api('GET', `/api/projects/${pid}/entries/deliverables/summary`); expect(sum.status).toBe(200); expect(sum.data.summary.length).toBe(7); expect(sum.data.totalRequired).toBe(6); const res = await fetch(`${BASE}/api/projects/${pid}/entries/deliverables/export`, { headers: headers() }); expect(res.status).toBe(200); const buf = await res.arrayBuffer(); const bytes = new Uint8Array(buf); expect(bytes[0]).toBe(0xEF); // UTF-8 BOM(服务端发送正确;fetch.text() 会剥 BOM 故查原始字节) expect(bytes[1]).toBe(0xBB); expect(bytes[2]).toBe(0xBF); const text = new TextDecoder().decode(buf); expect(text).toContain('源代码'); expect(text).toContain('演示录屏'); }); it('TC-PROJ-DEL: 含未完成条目时非 force 删除 409,force 成功', async () => { const r = await api('DELETE', `/api/projects/${pid}`); expect(r.status).toBe(409); const f = await api('DELETE', `/api/projects/${pid}?force=true`); expect(f.status).toBe(200); }); }); describe('Cancel Review(§3.2)', () => { let pid = ''; let eid = ''; beforeAll(async () => { const proj = await api('POST', '/api/projects', { name: `cancel-${Date.now()}`, track: '赛道一' }); pid = proj.data.id; await api('POST', `/api/projects/${pid}/standards`, { name: 'c-std', content: '## 场景价值(8分)' }); const e = await api('POST', `/api/projects/${pid}/entries`, { title: 'c-entry', repo_url: `file://C:\\c-${Date.now()}` }); eid = e.data.id; }); afterAll(async () => { await api('DELETE', `/api/projects/${pid}?force=true`); }); it('TC-CANCEL-01: queued 状态可取消 → cancelled', async () => { const db = (await import('../db')).default; db.prepare("UPDATE entries SET status = 'queued' WHERE id = ?").run(eid); const r = await api('POST', `/api/projects/${pid}/entries/${eid}/cancel`); expect(r.status).toBe(200); const d = await api('GET', `/api/projects/${pid}/entries/${eid}`); expect(d.data.status).toBe('cancelled'); }); it('TC-CANCEL-02: pending 状态不可取消 → 409', async () => { const db = (await import('../db')).default; db.prepare("UPDATE entries SET status = 'pending' WHERE id = ?").run(eid); const r = await api('POST', `/api/projects/${pid}/entries/${eid}/cancel`); expect(r.status).toBe(409); }); }); describe('赛道一子类型标准匹配(§3.3.2)', () => { it('TC-SUBTYPE-01: sub_type=新規 命中对应标准;无 sub_type 回落默认标准', async () => { const proj = await api('POST', '/api/projects', { name: `sub-${Date.now()}`, track: '赛道一' }); const pid = proj.data.id; const list = await api('GET', `/api/projects/${pid}/standards`); for (const s of list.data) await api('DELETE', `/api/projects/${pid}/standards/${s.id}`); await api('POST', `/api/projects/${pid}/standards`, { name: '新規标准', category_tag: '新規', content: '## 新規维度(5分)' }); await api('POST', `/api/projects/${pid}/standards`, { name: '修正标准', category_tag: '修正', content: '## 修正维度(5分)' }); await api('POST', `/api/projects/${pid}/standards`, { name: '默认标准', category_tag: '', content: '## 默认维度(5分)' }); const e1 = await api('POST', `/api/projects/${pid}/entries`, { title: 'sub-new', repo_url: `file://C:\\sn-${Date.now()}`, sub_type: '新規' }); const snap1 = JSON.parse(e1.data.standard_snapshot); expect(snap1[0].name).toBe('新規维度'); const e2 = await api('POST', `/api/projects/${pid}/entries`, { title: 'sub-def', repo_url: `file://C:\\sd-${Date.now()}` }); const snap2 = JSON.parse(e2.data.standard_snapshot); expect(snap2[0].name).toBe('默认维度'); await api('DELETE', `/api/projects/${pid}?force=true`); }); }); describe('Cleanup', () => { it('should delete project with force', async () => { const { status } = await api('DELETE', `/api/projects/${projectId}?force=true`); expect(status).toBe(200); }); }); describe('Password Change API', () => { it('TC-PWD-01: should reject without token', async () => { const saved = token; token = ''; const { status } = await api('POST', '/api/auth/password', { currentPassword: 'x', newPassword: 'abcdef' }); token = saved; expect(status).toBe(401); }); it('TC-PWD-02: should reject wrong current password', async () => { const { status, data } = await api('POST', '/api/auth/password', { currentPassword: 'wrong-current', newPassword: 'abcdef123' }); expect(status).toBe(403); expect(data.error).toContain('当前密码错误'); }); it('TC-PWD-03: should reject short new password', async () => { const { config } = await import('../config'); const { status, data } = await api('POST', '/api/auth/password', { currentPassword: config.authPassword, newPassword: '123' }); expect(status).toBe(400); expect(data.error).toContain('至少 6 位'); }); it('TC-PWD-04: should change password, enforce on login, and restore', async () => { const { config } = await import('../config'); const original = config.authPassword; const newPwd = 'new-pass-12345'; try { const changeRes = await api('POST', '/api/auth/password', { currentPassword: original, newPassword: newPwd }); expect(changeRes.status).toBe(200); expect(changeRes.data.success).toBe(true); const oldLogin = await api('POST', '/api/auth/login', { password: original }); expect(oldLogin.status).toBe(401); const newLogin = await api('POST', '/api/auth/login', { password: newPwd }); expect(newLogin.status).toBe(200); expect(newLogin.data.token).toBeTruthy(); token = newLogin.data.token; // 改密轮换了 AUTH_SECRET,旧 token 已失效 } finally { const restoreRes = await api('POST', '/api/auth/password', { currentPassword: newPwd, newPassword: original }); expect(restoreRes.status).toBe(200); const finalLogin = await api('POST', '/api/auth/login', { password: original }); expect(finalLogin.status).toBe(200); token = finalLogin.data.token; } }); });