Files
L2keka/web/e2e/security-fixes.spec.ts
T

138 lines
6.2 KiB
TypeScript

import { test, expect, type Page } from '@playwright/test';
const PASSWORD = 'test123';
const UNIQUE = Date.now().toString(36);
let pid = '';
let lastNewPwd = '';
async function login(page: Page, password = PASSWORD) {
await page.goto('/login');
await page.fill('input[type="password"]', password);
await page.click('button[type="submit"]');
await page.waitForURL('/');
}
// ══════════════════════════════════════
// K7: 改密流程(httpOnly cookie + 密钥轮换 → 需重登)
// ══════════════════════════════════════
test.describe('K7 改密流程', () => {
test.afterAll(async ({ request }) => {
// 崩溃安全:若改密测试中途失败,确保服务器密码还原,避免级联影响其他 spec
const ok = await request.post('http://localhost:3002/api/auth/login', { data: { password: PASSWORD } });
if (ok.ok()) return;
if (!lastNewPwd) return;
const loginNew = await request.post('http://localhost:3002/api/auth/login', { data: { password: lastNewPwd } });
if (!loginNew.ok()) return;
const { token } = await loginNew.json();
await request.post('http://localhost:3002/api/auth/password', {
headers: { Authorization: `Bearer ${token}` },
data: { currentPassword: lastNewPwd, newPassword: PASSWORD },
});
});
test('改密:当前密码错误时显示错误且不跳转', async ({ page }) => {
await login(page);
await page.click('button[title="修改管理密码"]');
await page.fill('input[placeholder="当前密码"]', 'wrong-current');
await page.fill('input[placeholder="新密码(至少 6 位)"]', 'new-pass-123');
await page.click('.new-project-actions button:has-text("保存")');
await expect(page.locator('.new-project-form')).toContainText('当前密码错误');
await expect(page).toHaveURL('/');
});
test('改密成功后旧密码失效、新密码可登录,并还原原密码', async ({ page }) => {
const NEW_PWD = `new-${UNIQUE}-x9`;
lastNewPwd = NEW_PWD;
await login(page, PASSWORD);
await page.click('button[title="修改管理密码"]');
await page.fill('input[placeholder="当前密码"]', PASSWORD);
await page.fill('input[placeholder="新密码(至少 6 位)"]', NEW_PWD);
await page.click('.new-project-actions button:has-text("保存")');
await page.waitForURL('/login');
// 旧密码登录失败
await page.fill('input[type="password"]', PASSWORD);
await page.click('button[type="submit"]');
await expect(page.locator('.error')).toContainText('密码错误');
// 新密码登录成功
await login(page, NEW_PWD);
await expect(page.locator('.sidebar-header h2')).toHaveText('AI-Review');
// 还原原密码
await page.click('button[title="修改管理密码"]');
await page.fill('input[placeholder="当前密码"]', NEW_PWD);
await page.fill('input[placeholder="新密码(至少 6 位)"]', PASSWORD);
await page.click('.new-project-actions button:has-text("保存")');
await page.waitForURL('/login');
await login(page, PASSWORD);
await expect(page.locator('.sidebar-header h2')).toHaveText('AI-Review');
});
});
// ══════════════════════════════════════
// D2: CSV 导入模板下载
// ══════════════════════════════════════
test.describe('D2 CSV 模板下载', () => {
test.beforeAll(async ({ request }) => {
const loginRes = await request.post('http://localhost:3002/api/auth/login', { data: { password: PASSWORD } });
const { token } = await loginRes.json();
const proj = await request.post('http://localhost:3002/api/projects', {
headers: { Authorization: `Bearer ${token}` },
data: { name: `csv-${UNIQUE}`, track: '赛道一' },
});
pid = (await proj.json()).id;
});
test('批量导入面板可下载 CSV 模板', async ({ page }) => {
await login(page);
await page.goto(`/project/${pid}`);
await page.click('button:has-text("批量导入")');
const downloadPromise = page.waitForEvent('download');
await page.click('button:has-text("下载模板")');
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe('entry-import-template.csv');
});
});
// ══════════════════════════════════════
// K4: 条目编辑保存补齐 sub_type/question_id
// ══════════════════════════════════════
test.describe('K4 编辑保存字段', () => {
let pid = '';
let eid = '';
test.beforeAll(async ({ request }) => {
const loginRes = await request.post('http://localhost:3002/api/auth/login', { data: { password: PASSWORD } });
const { token } = await loginRes.json();
const proj = await request.post('http://localhost:3002/api/projects', {
headers: { Authorization: `Bearer ${token}` }, data: { name: `k4-${UNIQUE}`, track: '赛道一' },
});
pid = (await proj.json()).id;
const entry = await request.post(`http://localhost:3002/api/projects/${pid}/entries`, {
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
data: { title: 'k4-entry', repo_url: `file://C:\\k4-${UNIQUE}` },
});
eid = (await entry.json()).id;
});
test('编辑保存 sub_type 持久化', async ({ page }) => {
await login(page);
await page.goto(`/project/${pid}`);
await page.click('button:has-text("编辑")');
await page.selectOption('.detail-overlay select', { label: '新規开发' });
await page.click('.detail-overlay button:has-text("保存")');
const loginRes = await page.request.post('http://localhost:3002/api/auth/login', { data: { password: PASSWORD } });
const { token } = await loginRes.json();
const detailRes = await page.request.get(`http://localhost:3002/api/projects/${pid}/entries/${eid}`, {
headers: { Authorization: `Bearer ${token}` },
});
const detail = await detailRes.json();
expect(detail.sub_type).toBe('新規');
});
});