0

I am trying to test a Hapi.js plugin with registration function:

exports.register = function(server, options, next) {

    server.route({
        method: 'POST',
        path: '/register',
        config: {
            payload: {
                allow: 'application/json'
            },
            validate: {
                /* deleted for brevity */
            }
        },

        handler: function(request, reply) {

            if (!server.app.mongoose) {

                server.log('error', 'Failed to find an active MongoDB connection.');

                return reply(Boom.badImplementation());
            }

            var response = reply().hold();

            var mongoose = server.app.mongoose;

            var User = mongoose.model('User');

            var password = request.payload.password;

            return new Promise(function(resolve, reject) {

                bcrypt.genSalt(10, function(err, salt) {

                    if (err) {

                        server.log('error', 'Failed to generate bcrypt salt: ' + err);

                        return reject();
                    }

                    bcrypt.hash(password, salt, function(err, hash) {

                        /* deleted for brevity */

                        user.save(function(err, savedUser) {

                            if (err) {

                                server.log('error', 'Failed to save user to the database: ' + err);

                                return reject(Boom.conflict());
                            }

                            server.log('debug', 'Registered new user with e-mail validation code: ' + validationCode);

                            resolve({});
                        });
                    });
                });

            }).then(function(data) {

                response.statusCode = 201;
                response.source = data;
                response.send();

                return response;

            }, function(err) {

                if (!err)
                    err = Boom.badImplementation();

                response.statusCode = err.output.statusCode;
                response.source = err.output.payload;
                response.send();

                return response;
            });
        }
    });

    next();
};

My test file is here:

const Lab = require('lab');
const expect = require('code').expect;

const server = require('../');
const lab = exports.lab = Lab.script();

const mongoose = require('../plugins/mongo.js').mongoose;

lab.experiment('Registration', function() {

    lab.before(function(done) {

        mongoose.connection.collections['users'].drop(function(err, resp) {

            if (err) {

                console.error(err);

            } else {

                console.log(resp);
            }
        });

        var User = mongoose.model('User');

        /* deleted for brevity */

        user.save(function(err, savedUser) {

            if (err) {

                server.log('error', 'Failed to save user to the database: ' + err);

                done(err);
            }

            done();
        });
    });

    lab.test('/register endpoint with empty payload', function(done) {

        server.inject({
            method: 'POST',
            url: '/register',
            payload: {}
        }, function(response) {

            expect(response.statusCode).to.be.equal(400);
            expect(response.result.message).to.match(/^child "\w+" fails because \["\w+" is required\]$/);

            done();
        });
    });

    lab.test('/register endpoint with invalid email', function(done) {

        server.inject({
            method: 'POST',
            url: '/register',
            payload: {
               ...
            }
        }, function(response) {

            expect(response.statusCode).to.be.equal(400);
            expect(response.result.message).to.be.equal('child "email" fails because ["email" must be a valid email]');

            done();
        });
    });

    lab.test('/register endpoint with short password', function(done) {

        server.inject({
            method: 'POST',
            url: '/register',
            payload: {
                ...
            }
        }, function(response) {

            expect(response.statusCode).to.equal(400);
            expect(response.result.message).to.startWith('child "password" fails because ["password" length must be at least');

            done();
        });
    });

    lab.test('/register endpoint with invalid password', function(done) {

        server.inject({
            method: 'POST',
            url: '/register',
            payload: {
                ...
            }
        }, function(response) {

            expect(response.statusCode).to.equal(400);
            expect(response.result.message).to.startWith('child "password" fails because');

            done();
        });
    });

    lab.test('/register endpoint with existing username', function(done) {

        server.inject({
            method: 'POST',
            url: '/register',
            payload: {
                ...
            }
        }, function(response) {

            expect(response.statusCode).to.equal(409);
            done();
        });
    });

    lab.test('/register endpoint with valid payload', function(done) {

        server.inject({
            method: 'POST',
            url: '/register',
            payload: {
                ...
            }
        }, function(response) {

            expect(response.statusCode).to.equal(201);

            done();
        });
    });
});

Everything was fine until I added the '/register endpoint with existing username'.

Now subsequent invocations of lab command exits with success and then a failure for the tests 5 and 6 (it goes on and on like this, one success and then one failure). Basically, it looks like resulting status codes are swapped after a successful test launch.

Looks like a synchronization issue but I could not find where the cause is. Any ideas?

Gergo Erdosi
  • 40,904
  • 21
  • 118
  • 94
Deniz Acay
  • 1,609
  • 1
  • 13
  • 24
  • Not seen reply().hold() syntax before but why are you holding onto response anyway? The api docs say to wrap promise into an object which can then be used in the reply callback. – simon-p-r Feb 22 '16 at 09:50
  • @simon-p-r Because of the statement "framework will resume as soon as the handler method exits" in API docs: http://hapijs.com/api#flow-control. Also this question provided similar examples: http://stackoverflow.com/questions/29402797/how-to-reply-from-outside-of-the-hapi-js-route-handler – Deniz Acay Feb 23 '16 at 00:53
  • The last answer in question shows how you need to return promise, I take it you have tried that? I don't use mongoose but does the user callback need to be nested inside the connection callback. – simon-p-r Feb 23 '16 at 09:16
  • 1
    try using `lab.beforeEach` instead of `lab.before`. You're only cleaning up the db once at the start of your test run instead of between each test. So you have shared state and your tests are not isolated from each other. – Robbie Mar 02 '16 at 23:29
  • also in your `before`, if your `save` finishes before your `drop`, then your user will be deleted. That could potentially give inconsistent results. – Robbie Mar 02 '16 at 23:32

2 Answers2

1

In your before step, move the save inside the drop callback.

In your current code, there is a chance the save will finish before the drop does. So the user might not exists which would cause tests 5 and 6 to fail and succeed alternately

Robbie
  • 18,750
  • 4
  • 41
  • 45
0

I managed to solve this problem by seperating server instances per tests and registering only the plugin(s) to be tested as recommended by the doc.

I am answering my own question for anyone who may encounter the same problem in the future.

Deniz Acay
  • 1,609
  • 1
  • 13
  • 24