如何使用Jest和Supertest测试Passport.js本地认证
问题描述
需要为Passport.js本地认证逻辑编写测试时遇到以下问题:
- 默认配置下,认证成功、失败均返回302状态码触发重定向,状态码完全一致,无法直接通过状态码区分认证结果
- 尝试编写自定义认证回调逻辑,在认证失败时返回401未授权状态码并重定向到
/users/login,但前端不会自动完成跳转,只会显示附带跳转路由链接的页面 - 后续尝试通过断言
response.headers['Location']判断重定向地址,但实际获取到的Location值为undefined
最初使用的自定义认证路由代码:
/* app.post('/users/login', passport.authenticate('local', { successRedirect: '/users/dashboard', failureRedirect: '/users/login', failureFlash: true, }));*/ app.post('/users/login', (req, res, next) => { passport.authenticate('local', (err, user, info) => { if (err) return next(err); if (!user) res.redirect(401, '/users/login'); // failure req.logIn(user, (err) => { if (err) return next(err); return res.redirect('/users/dashboard'); //success }); })(req, res, next); });
最初编写的测试用例(仅能断言状态码,不符合前端实际跳转逻辑):
test('should return a status code: 401', async () => { const response = await request(app).post('/users/login').send({ // incorrect login email: 'jane@gmail.com', password: 'asdfa' }) expect(response.statusCode).toBe(401) })
后续调整的测试用例(无法获取到Location头):
test('should redirect to /users/login', async () => { const response = await request(app).post('/users/login').send({ email: 'jane@gmail.com', password: 'incorrectPassword' }) expect(response.headers['Location']).toEqual('/users/login'); })
回退到默认配置的登录路由:
app.post('/users/login', passport.authenticate('local', { successRedirect: '/users/dashboard', failureRedirect: '/users/login', failureFlash: true }));
解决方案
核心原因说明
- 401状态码不会触发浏览器自动跳转:HTTP规范中仅3xx系列的重定向状态码会被浏览器自动识别并执行跳转,401属于客户端错误状态码,即使携带Location头,浏览器也不会自动跳转,这就是自定义回调返回401+重定向时前端不跳的根本原因,这种写法不符合HTTP语义,生产环境不推荐使用。
- 拿不到Location头是因为Supertest默认自动跟随重定向:Supertest发起请求时默认会自动跟进3xx重定向,最终返回的是重定向后目标页面的响应,自然不会携带Location头。
正确测试写法(服务端渲染+重定向场景)
不需要修改Passport默认的302重定向逻辑,测试时添加.redirects(0)配置禁止Supertest自动跟随重定向,即可拿到原始重定向响应,同时通过状态码+Location头判断认证结果即可。
- 登录失败场景测试:
test('登录失败时重定向到登录页', async () => { const response = await request(app) .post('/users/login') .send({ email: 'jane@gmail.com', password: 'incorrectPassword' }) .redirects(0) // 关键配置:禁止自动跟随重定向 expect(response.statusCode).toBe(302) expect(response.headers.location).toBe('/users/login') })
- 登录成功场景测试:
test('登录成功时重定向到控制台页', async () => { const response = await request(app) .post('/users/login') .send({ email: 'correct@example.com', password: 'correctPassword' }) .redirects(0) expect(response.statusCode).toBe(302) expect(response.headers.location).toBe('/users/dashboard') // 可额外断言session中是否存入用户信息,确认登录状态生效 })
额外注意事项
之前写的自定义认证回调存在逻辑漏洞:判断!user的分支执行res.redirect后没有加return阻断流程,代码会继续向下执行req.logIn逻辑,引发请求处理错乱,如果要使用自定义回调,需要补全return:
if (!user) { return res.redirect(401, '/users/login'); // 加return终止后续逻辑执行 }
如果是前后端分离架构,不需要重定向的场景,可以直接在自定义回调里返回JSON响应:认证失败返回401+错误信息,认证成功返回200+用户信息,测试时直接断言状态码和响应体即可,不需要处理重定向逻辑。
内容的提问来源于stack exchange,提问作者NoobSailboat
相关产品推荐
相关产品推荐

