【问题标题】:How to authenticate Supertest requests with Passport?如何使用 Passport 验证 Supertest 请求?
【发布时间】:2012-12-09 16:27:21
【问题描述】:

我正在使用 Passport.js 进行身份验证(本地策略)并使用 Mocha 和 Supertest 进行测试。

如何使用 Supertest 创建会话并发出经过身份验证的请求?

【问题讨论】:

    标签: node.js mocha.js supertest passport.js


    【解决方案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.cookieset 的调用。这将导致请求使用我们构建的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();
            });
        })
        

        【讨论】:

          猜你喜欢
          • 2014-01-05
          • 2018-08-24
          • 1970-01-01
          • 1970-01-01
          • 2020-12-11
          • 2016-03-20
          • 1970-01-01
          • 1970-01-01
          • 2023-03-17
          相关资源
          最近更新 更多