如何使用 Passport 验证 Supertest 请求?
Posted
技术标签:
【中文标题】如何使用 Passport 验证 Supertest 请求?【英文标题】:How to authenticate Supertest requests with Passport? 【发布时间】:2012-12-09 16:27:21 【问题描述】:我正在使用 Passport.js 进行身份验证(本地策略)并使用 Mocha 和 Supertest 进行测试。
如何使用 Supertest 创建会话并发出经过身份验证的请求?
【问题讨论】:
【参考方案1】:您应该为此使用superagent。它是较低级别的模块,由supertest
使用。看看Persisting an agent部分:
var request = require('superagent');
var user1 = request.agent();
user1
.post('http://localhost:4000/signin')
.send( user: 'hunter@hunterloftis.com', password: 'password' )
.end(function(err, res)
// user1 will manage its own cookies
// res.redirects contains an Array of redirects
);
现在您可以使用user1
发出经过身份验证的请求。
【讨论】:
使用这种方法我需要运行一个测试服务器。是否可以将它与 Supertest 的服务器一起使用?我正在使用会话 cookie(带有 Passport)但它不起作用,我查看了 user1.post 的响应,cookie 不包含用户信息 您不需要测试服务器。您可以使用您的普通快递 app.js。你看过example吗?如果您想将测试保存在单独的文件中,请将require(../app.js)
放入标题中以启动您的应用程序。
我让它工作,但前提是我杀死已经运行的开发服务器。有了 supertest,我就不必这样做了。任何想法如何使它与 superagent 很好地配合?也许为测试环境监听不同的端口?
那么我如何发出另一个请求并在其他 it("should create an object by this user1")
测试中使用该 user1 会话?
您可以将服务器运行的端口号绑定到环境变量,并将运行测试时的端口号设置为服务器未使用的端口号。【参考方案2】:
试试这个,
var request=require('supertest');
var cookie;
request(app)
.post('/login')
.send( email: "user@gluck.com", password:'password' )
.end(function(err,res)
res.should.have.status(200);
cookie = res.headers['set-cookie'];
done();
);
//
// and use the cookie on the next request
request(app)
.get('/v1/your/path')
.set('cookie', cookie)
.end(function(err,res)
res.should.have.status(200);
done();
);
【讨论】:
第二次请求请求永远不会触发。也就是说,永远不会到达 .end 处理程序。 如果将第二个请求放在第一个结束回调中,这会很好。 很抱歉投了反对票,但按照安迪的回答,request.agent(app)
比手动设置 cookie 优雅得多。
我的会话 api 没有设置 cookie。它返回客户端存储的用户对象。
我在路由外部设置了一个变量并将其分配在内部并使用它来验证.expect(res => cookie = res.headers["set-cookie"]; )
【参考方案3】:
我将假设您使用的是 CookieSession 中间件。
正如 grub 提到的,您的目标是获取一个 cookie 值以传递给您的请求。但是,无论出于何种原因(至少在我的测试中), supertest 都不会在同一个测试中触发 2 个请求。因此,我们必须对如何获得正确的 cookie 值进行逆向工程。首先,您需要 require 用于构建 cookie 的模块:
var Cookie = require("express/node_modules/connect/lib/middleware/session/cookie")
, cookieSignature = require("express/node_modules/cookie-signature")
是的,这很难看。我把它们放在我的测试文件的顶部。
接下来,我们需要构造 cookie 值。对于需要经过身份验证的用户的测试,我将其放入 beforeEach
:
var cookie = new Cookie()
, session =
passport:
user: Test.user.id
var val = "j:" + JSON.stringify(session)
val = 's:' + cookieSignature.sign(val, App.config.cookieSecret)
Test.cookie = cookie.serialize("session",val)
Test.user.id
之前定义在我的beforeEach
链的一部分中,该部分定义了我要“登录”的用户。 session
的结构是 Passport(至少目前)如何将当前用户信息插入您的会话。
带有"j:"
和"s:"
的var val
行从Connect CookieSession 中间件中删除,如果您使用的是基于cookie 的会话,Passport 将使用该中间件。最后,我们序列化 cookie。我把"session"
放在那里,因为这就是我配置我的cookie 会话中间件的方式。此外,App.config.cookieSecret
在别处定义,它必须是您传递给 Express/Connect CookieSession 中间件的秘密。我将它存储到 Test.cookie
以便我以后可以访问它。
现在,在实际测试中,您需要使用该 cookie。例如,我有以下测试:
it("should logout a user", function(done)
r = request(App.app)
.del(App.Test.versionedPath("/logout"))
.set("cookie", Test.cookie)
// ... other sets and expectations and your .end
注意使用"cookie"
和Test.cookie
对set
的调用。这将导致请求使用我们构建的cookie。
现在您已经假装您的应用认为用户已登录,而您不必保持实际服务器运行。
【讨论】:
另外,您可以直接测试您的请求处理程序,将一些虚拟的 req 和 res 对象传递给它。当然,这不会测试您的路由。【参考方案4】:正如 zeMirco 所指出的,底层的 superagent
模块支持会话,自动为您维护 cookie。但是,可以通过未记录的功能使用 supertest
中的 superagent.agent()
功能。
只需使用require('supertest').agent('url')
而不是require('supertest')('url')
:
var request = require('supertest');
var server = request.agent('http://localhost:3000');
describe('GET /api/getDir', function()
it('login', loginUser());
it('uri that requires user to be logged in', function(done)
server
.get('/api/getDir')
.expect(200)
.end(function(err, res)
if (err) return done(err);
console.log(res.body);
done()
);
);
);
function loginUser()
return function(done)
server
.post('/login')
.send( username: 'admin', password: 'admin' )
.expect(302)
.expect('Location', '/')
.end(onResponse);
function onResponse(err, res)
if (err) return done(err);
return done();
;
;
【讨论】:
如果您将 app.js 放入request.agent(app);
,它可以在没有运行服务器的情况下工作。很酷的东西。
这让我摆脱了 3 天的存根、嘲笑、需要缓存清理和灵魂粉碎尝试的地狱......干杯!【参考方案5】:
作为 Andy 答案的补充,为了让 Supertest 为您启动服务器,您可以这样做:
var request = require('supertest');
/**
* `../server` should point to your main server bootstrap file,
* which has your express app exported. For example:
*
* var app = express();
* module.exports = app;
*/
var server = require('../server');
// Using request.agent() is the key
var agent = request.agent(server);
describe('Sessions', function()
it('Should create a session', function(done)
agent.post('/api/session')
.send( username: 'user', password: 'pass' )
.end(function(err, res)
expect(req.status).to.equal(201);
done();
);
);
it('Should return the current session', function(done)
agent.get('/api/session').end(function(err, res)
expect(req.status).to.equal(200);
done();
);
);
);
【讨论】:
应该是expect(res.status)
而不是req.status
。
最佳答案。
这对我有用,使用护照 LocalStrategy 进行身份验证。就我而言,还需要进行两项更改。首先,我必须更改 afterEach()
以便它删除除用户之外的所有集合。其次,我必须使用 --runInBand
选项调用 jest,这会使测试按列出的顺序运行。
我的代码:var request = require("supertest"), app = require("../app"), agent = request.agent(app); describe("Notifications", () => const username = "u", pwd = "p"; let user; it("logs in", async () user = new User(username); user.setPassword(pwd); await user.save(); agent.post('/login').send(username, pwd).expect(302); ); it('shows notification', async () => const msg = "msg"; const n = new Notification(user, msg); await n.save(); agent.get("/").expect(200).end(function(err,res) if(err) return err; expect(res.text).toMatch(msg); ); );
【参考方案6】:
很抱歉,建议的解决方案都不适合我。
使用supertest.agent()
我不能使用app
实例,我需要预先运行服务器并指定http://127.0.0.1:port
,而且我不能使用supertest的期望(断言),我不能使用supertest-as-promised
lib 等等...
cookies
的案例对我根本不起作用。
所以,我的解决方案是:
如果您使用的是 Passport.js,它会利用“Bearer token”机制,您可以在规范中使用以下示例:
var request = require('supertest');
var should = require('should');
var app = require('../server/app.js'); // your server.js file
describe('Some auth-required API', function ()
var token;
before(function (done)
request(app)
.post('/auth/local')
.send(
email: 'test@example.com',
password: 'the secret'
)
.end(function (err, res)
if (err)
return done(err);
res.body.should.to.have.property('token');
token = res.body.token;
done();
);
);
it('should respond with status code 200 and so on...', function (done)
request(app)
.get('/api/v2/blah-blah')
.set('authorization', 'Bearer ' + token) // 1) using the authorization header
.expect(200)
.expect('Content-Type', /json/)
.end(function (err, res)
if (err)
return done(err);
// some `res.body` assertions...
done();
);
);
it('should respond with status code 200 and so on...', function (done)
request(app)
.get('/api/v2/blah-blah')
.query(access_token: token) // 2) using the query string
.expect(200)
.expect('Content-Type', /json/)
.end(function (err, res)
if (err)
return done(err);
// some `res.body` assertions...
done();
);
);
);
您可能需要一个辅助函数来验证用户身份:
test/auth-helper.js
'use strict';
var request = require('supertest');
var app = require('app.js');
/**
* Authenticate a test user.
*
* @param User user
* @param function(err:Error, token:String) callback
*/
exports.authenticate = function (user, callback)
request(app)
.post('/auth/local')
.send(
email: user.email,
password: user.password
)
.end(function (err, res)
if (err)
return callback(err);
callback(null, res.body.token);
);
;
度过充实的一天!
【讨论】:
【参考方案7】:这是一种简洁的方法,它具有可重复使用的额外好处。
const chai = require("chai")
const chaiHttp = require("chai-http")
const request = require("supertest")
const app = require("../api/app.js")
const should = chai.should()
chai.use(chaiHttp)
describe("a mocha test for an expressjs mongoose setup", () =>
// A reusable function to wrap your tests requiring auth.
const signUpThenLogIn = (credentials, testCallBack) =>
// Signs up...
chai
.request(app)
.post("/auth/wizard/signup")
.send(
name: "Wizard",
...credentials,
)
.set("Content-Type", "application/json")
.set("Accept", "application/json")
.end((err, res) =>
// ...then Logs in...
chai
.request(app)
.post("/auth/wizard/login")
.send(credentials)
.set("Content-Type", "application/json")
.set("Accept", "application/json")
.end((err, res) =>
should.not.exist(err)
res.should.have.status(200)
res.body.token.should.include("Bearer ")
// ...then passes the token back into the test
// callBack function.
testCallBack(res.body.token)
)
)
it.only("flipping works", done =>
// "Wrap" our test in the signUpThenLogIn function.
signUpLogIn(
// The credential parameter.
username: "wizard",
password: "youSHALLpass",
,
// The test wrapped in a callback function which expects
/// the token passed back from when signUpLogIn is done.
token =>
// Now we can use this token to run a test...
/// e.g. create an apprentice.
chai
.request(app)
.post("/apprentice")
.send( name: "Apprentice 20, innit" )
// Using the token to auth!
.set("Authorization", token)
.end((err, res) =>
should.not.exist(err)
res.should.have.status(201)
// Yep. apprentice created using the token.
res.body.name.should.be.equal("Apprentice 20, innit")
done()
)
)
)
)
奖励材料
为了使其更加可重用,将函数放入一个名为“myMochaSuite.js”的文件中,您可以在测试您的 api 服务器时将“describe”替换为该文件。做一个向导,把你之前/之后的所有东西都放在这个“套件”中。例如:
// tests/myMochaSuite.js
module.exports = (testDescription, testsCallBack) =>
describe(testDescription, () =>
const signUpThenLogIn = (credentials, testCallBack) =>
// The signUpThenLogIn function from above
before(async () =>
//before stuff like setting up the app and mongoose server.
)
beforeEach(async () =>
//beforeEach stuff clearing out the db
)
after(async () =>
//after stuff like shutting down the app and mongoose server.
)
// IMPORTANT: We pass signUpLogIn back through "testsCallBack" function.
testsCallBack(signUpThenLogIn)
)
// tests/my.api.test.js
// chai, supertest, etc, imports +
const myMochaSuite = require("./myMochaSuite")
// NB: signUpThenLogIn coming back into the tests.
myMochaSuite("my test description", signUpThenLogIn =>
it("just works baby", done =>
signUpThenLogIn(
username: "wizard", password: "youSHALLpass",
token =>
chai
.request(app)
.get("/apprentices/20")
// Using the incoming token passed when signUpThenLogIn callsback.
.set("Authorization", token)
.end((err, res) =>
res.body.name.equals("Apprentice 20, innit")
done()
)
)
)
)
现在您的所有测试都拥有一个更加可重用的套件“包装器”,让它们变得整洁。
【讨论】:
【参考方案8】:GraphQl 完整示例:
const adminLogin = async (agent) =>
const userAdmin = await User.findOne(rol:"admin").exec();
if(!userAdmin) return new Promise.reject('Admin not found')
return agent.post('/graphql').send(
query: ` mutation $loginQuery(userAdmin.email) `
)//.end((err, body:data) => )
test("Login Admin", async (done) =>
const agent = request.agent(app);
await adminLogin(agent);
agent
.post("/graphql")
.send(query: ` getGuests $GuestInput.join(' ') `)
.set("Accept", "application/json")
.expect("Content-Type", /json/)
.expect(200)
.end((err, body:data) =>
if (err) return done(err);
expect(data).toBeInstanceOf(Object);
const getGuests = data;
expect(getGuests).toBeInstanceOf(Array);
getGuests.map(user => GuestInput.map(checkFields(user)))
done();
);
)
【讨论】:
以上是关于如何使用 Passport 验证 Supertest 请求?的主要内容,如果未能解决你的问题,请参考以下文章
如何使用 Passport 对 GraphQL 端点进行身份验证?
如何使用 Laravel Passport (5.3) 记录身份验证尝试