// 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); }); });