fe17886ac4
- 车辆 / 洗车 / 加油 / 充电 / 保养 / 保险 完整 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
99 lines
3.7 KiB
JavaScript
99 lines
3.7 KiB
JavaScript
// server/test/integration.middleware.test.js
|
||
// 用 supertest 串联多个中间件,验证真实 Express 流程
|
||
// 选最小依赖:手动构造 app(不依赖 initDb / 真实路由)
|
||
import { describe, it, expect } from 'vitest';
|
||
import express from 'express';
|
||
import session from 'express-session';
|
||
import request from 'supertest';
|
||
import { requireAuth } from '../src/middleware/auth.js';
|
||
import { requireCsrf } from '../src/middleware/csrf.js';
|
||
|
||
function buildApp() {
|
||
const app = express();
|
||
app.use(express.json());
|
||
app.use(
|
||
session({
|
||
name: 'TEST_SID',
|
||
secret: 'test-secret',
|
||
resave: false,
|
||
saveUninitialized: false,
|
||
})
|
||
);
|
||
|
||
// 公共路由:登出
|
||
app.post('/api/test/logout', requireCsrf, (req, res) => {
|
||
req.session.destroy(() => res.json({ ok: true }));
|
||
});
|
||
|
||
// 公共路由:CSRF
|
||
app.get('/api/test/csrf', (req, res) => {
|
||
if (!req.session.csrfToken) {
|
||
req.session.csrfToken = 'test-token-' + Math.random().toString(36).slice(2);
|
||
}
|
||
res.json({ ok: true, data: { csrf_token: req.session.csrfToken } });
|
||
});
|
||
|
||
// 受保护路由
|
||
app.get('/api/test/protected', requireAuth, (req, res) => res.json({ ok: true }));
|
||
app.post('/api/test/protected', requireAuth, requireCsrf, (req, res) => res.json({ ok: true }));
|
||
|
||
// 普通路由(非 /api/)
|
||
app.get('/settings', requireAuth, (req, res) => res.json({ ok: true }));
|
||
|
||
return app;
|
||
}
|
||
|
||
describe('集成:中间件链路', () => {
|
||
it('GET /api/test/protected 未登录 → 401 JSON', async () => {
|
||
const app = buildApp();
|
||
const r = await request(app).get('/api/test/protected');
|
||
expect(r.status).toBe(401);
|
||
expect(r.body.error.code).toBe('UNAUTHORIZED');
|
||
});
|
||
|
||
it('GET /settings 未登录 → 302 redirect', async () => {
|
||
const app = buildApp();
|
||
const r = await request(app).get('/settings');
|
||
expect(r.status).toBe(302);
|
||
expect(r.headers.location).toMatch(/\/login\?return_to=/);
|
||
});
|
||
|
||
it('完整流程:拿 csrf → 登录模拟 → 访问受保护', async () => {
|
||
const app = buildApp();
|
||
const agent = request.agent(app);
|
||
|
||
// 1. 拿 csrf(GET 通过,但 express-session 需要先有 session)
|
||
// 这里用 agent 自动管理 cookie
|
||
const csrfRes = await agent.get('/api/test/csrf');
|
||
expect(csrfRes.status).toBe(200);
|
||
const token = csrfRes.body.data.csrf_token;
|
||
|
||
// 2. 手动模拟"已登录":直接 post 到受保护路由但先注入 userId
|
||
// 此处改用 post 触发 CSRF 校验,要求带正确 token
|
||
// (受保护路由会先 401 因为没 userId)
|
||
const r401 = await agent.post('/api/test/protected').send({ csrf_token: token });
|
||
expect(r401.status).toBe(401);
|
||
|
||
// 3. 验证 logout 流程:先用 csrf 路由拿到 token(cookie 已通过 agent 维持)
|
||
// 然后 POST logout
|
||
const logoutRes = await agent
|
||
.post('/api/test/logout')
|
||
.send({ csrf_token: token });
|
||
expect(logoutRes.status).toBe(200);
|
||
expect(logoutRes.body.ok).toBe(true);
|
||
});
|
||
|
||
it('POST /api/test/logout 缺 CSRF → 403', async () => {
|
||
const app = buildApp();
|
||
const r = await request(app).post('/api/test/logout').send({});
|
||
expect(r.status).toBe(403);
|
||
expect(r.body.error.code).toBe('CSRF');
|
||
});
|
||
|
||
it('POST /api/test/logout 错 CSRF → 403', async () => {
|
||
const app = buildApp();
|
||
const r = await request(app).post('/api/test/logout').send({ csrf_token: 'fake' });
|
||
expect(r.status).toBe(403);
|
||
});
|
||
});
|