65b0bb04f8
把 CarLog v2.8 全套源码 + 配置导入到 i 仓库作为 baseline: - server/src/ (13 个路由 + middleware + services + config) - server/migrations/ (0001~0018 共 18 个迁移 + mysql) - server/test/ (12 文件 101 测试) - client/src/ (20 个 view + components + stores + api + composables) - client/public/ + client/scripts/ - 全部配置文件 (.editorconfig, .eslintrc.json, .prettierrc.json, vitest.config.js, lighthouserc.json, .pa11yci.json, package.json, carlog-init.sql) - .husky/pre-commit (git hooks) - docs/install/ (宝塔部署文档) 不含: - node_modules/ (本地 npm install) - .env (敏感, 走 .env.example) - *.zip / *.log / *.sqlite / .DS_Store 新增文档 docs/DEV-PLAN.md: - Phase 1: 平台基座 (019 migration + 3 个 platform 路由 + 3 个 view) - Phase 2: CarLog 子系统化 (后端 routes/ → subsystems/carlog/ + 前端 views/ → views/subsystems/carlog/ + 元数据驱动菜单) - Phase 3: 验证 (测试 + E2E + DB 完整性) - 交付清单 + commit 模板 + 给 Mavis review 的材料 后续 Trae 实施, 提交后我 code review + 跑测试。
123 lines
4.0 KiB
JavaScript
123 lines
4.0 KiB
JavaScript
// server/test/middleware.csrf.test.js
|
||
// 测试 server/src/middleware/csrf.js 的所有分支
|
||
import { describe, it, expect, vi } from 'vitest';
|
||
import { requireCsrf } from '../src/middleware/csrf.js';
|
||
|
||
function mockRes() {
|
||
return {
|
||
statusCode: 200,
|
||
body: null,
|
||
status(c) { this.statusCode = c; return this; },
|
||
json(b) { this.body = b; return this; },
|
||
};
|
||
}
|
||
|
||
describe('middleware/requireCsrf', () => {
|
||
it('GET 请求直接放行', () => {
|
||
const req = { method: 'GET', session: { csrfToken: 'abc' } };
|
||
const res = mockRes();
|
||
const next = vi.fn();
|
||
requireCsrf(req, res, next);
|
||
expect(next).toHaveBeenCalledOnce();
|
||
expect(res.statusCode).toBe(200);
|
||
});
|
||
|
||
it('HEAD 请求直接放行', () => {
|
||
const req = { method: 'HEAD', session: { csrfToken: 'abc' } };
|
||
const next = vi.fn();
|
||
requireCsrf(req, mockRes(), next);
|
||
expect(next).toHaveBeenCalledOnce();
|
||
});
|
||
|
||
it('OPTIONS 请求直接放行', () => {
|
||
const req = { method: 'OPTIONS', session: { csrfToken: 'abc' } };
|
||
const next = vi.fn();
|
||
requireCsrf(req, mockRes(), next);
|
||
expect(next).toHaveBeenCalledOnce();
|
||
});
|
||
|
||
it('POST 没有 session.csrfToken → 403', () => {
|
||
const req = { method: 'POST', body: { csrf_token: 'abc' } };
|
||
const res = mockRes();
|
||
const next = vi.fn();
|
||
requireCsrf(req, res, next);
|
||
expect(next).not.toHaveBeenCalled();
|
||
expect(res.statusCode).toBe(403);
|
||
expect(res.body.error.code).toBe('CSRF');
|
||
});
|
||
|
||
it('POST session 没 csrfToken 但用户提交了 → 403', () => {
|
||
const req = { method: 'POST', body: { csrf_token: 'abc' }, session: {} };
|
||
const res = mockRes();
|
||
requireCsrf(req, res, vi.fn());
|
||
expect(res.statusCode).toBe(403);
|
||
});
|
||
|
||
it('POST 错误 token → 403', () => {
|
||
const req = {
|
||
method: 'POST',
|
||
body: { csrf_token: 'wrong' },
|
||
session: { csrfToken: 'right' },
|
||
};
|
||
const res = mockRes();
|
||
requireCsrf(req, res, vi.fn());
|
||
expect(res.statusCode).toBe(403);
|
||
expect(res.body.error.message).toMatch(/校验失败/);
|
||
});
|
||
|
||
it('POST 正确 token(body)→ 放行', () => {
|
||
const req = {
|
||
method: 'POST',
|
||
body: { csrf_token: 'good' },
|
||
session: { csrfToken: 'good' },
|
||
};
|
||
const next = vi.fn();
|
||
requireCsrf(req, mockRes(), next);
|
||
expect(next).toHaveBeenCalledOnce();
|
||
});
|
||
|
||
it('POST 正确 token(header)→ 放行', () => {
|
||
const req = {
|
||
method: 'POST',
|
||
body: {},
|
||
headers: { 'x-csrf-token': 'good' },
|
||
session: { csrfToken: 'good' },
|
||
};
|
||
const next = vi.fn();
|
||
requireCsrf(req, mockRes(), next);
|
||
expect(next).toHaveBeenCalledOnce();
|
||
});
|
||
|
||
it('PUT 也需校验', () => {
|
||
const req = { method: 'PUT', body: {}, headers: {}, session: { csrfToken: 'x' } };
|
||
const res = mockRes();
|
||
requireCsrf(req, res, vi.fn());
|
||
expect(res.statusCode).toBe(403);
|
||
});
|
||
|
||
it('DELETE 也需校验', () => {
|
||
const req = { method: 'DELETE', body: {}, headers: {}, session: { csrfToken: 'x' } };
|
||
const res = mockRes();
|
||
requireCsrf(req, res, vi.fn());
|
||
expect(res.statusCode).toBe(403);
|
||
});
|
||
|
||
it('长度为 0 的 token(防御性)→ 抛错或 403', () => {
|
||
const req = {
|
||
method: 'POST',
|
||
body: { csrf_token: '' },
|
||
session: { csrfToken: 'good' },
|
||
};
|
||
const res = mockRes();
|
||
// 防御:空 token 走 timingSafeEqual 会抛错,被 require() 内部 try/catch 吞掉
|
||
// 期望:不调用 next
|
||
try {
|
||
requireCsrf(req, res, vi.fn());
|
||
expect(res.statusCode).toBe(403);
|
||
} catch {
|
||
// 也接受抛错(更安全的行为)
|
||
expect(true).toBe(true);
|
||
}
|
||
});
|
||
});
|