Files
CarLog/server/test/middleware.csrf.test.js
T
wsh5485 fe17886ac4 feat: 洗车管理系统 v2.8 — 个人 detailer 单用户全栈应用
- 车辆 / 洗车 / 加油 / 充电 / 保养 / 保险 完整 CRUD + 软删
- AI 截图识别(5 类型 OCR schema):OpenAI 兼容 + MiniMax M3
- 化学品 / Grocy 实例对接 + 库存镜像同步
- 仪表盘:30 天频次 + 健康度 + 同比环比 + 油价趋势 + 年均养护
- 月度报表:Excel 6 sheet + PDF
- PWA:manifest / SW / 离线缓存 / iOS 引导
- 安全:bcrypt + CSRF + 登录锁定(IP/用户/全局三级)+ 401 自动跳登录 + 表单草稿
- 高 ROI 8 功能:里程/提醒/成本/搜索/标签/通知/同比/成就
- 3 个新 migration(0016/0017/0018)+ 18 个迁移全幂等
- 101/101 测试通过(含 ipRateLimit / CSRF / retry / stats / tags / notifications)
- 部署:宝塔面板文档 + PM2 + Nginx
2026-06-20 21:11:54 +08:00

123 lines
4.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 正确 tokenbody)→ 放行', () => {
const req = {
method: 'POST',
body: { csrf_token: 'good' },
session: { csrfToken: 'good' },
};
const next = vi.fn();
requireCsrf(req, mockRes(), next);
expect(next).toHaveBeenCalledOnce();
});
it('POST 正确 tokenheader)→ 放行', () => {
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);
}
});
});