From 11799339cfa78595fd1f1043f386d468d254d321 Mon Sep 17 00:00:00 2001 From: Damien Lebreuilly Date: Mon, 20 Apr 2015 13:24:14 +0200 Subject: [PATCH 1/7] added plugin private.js: registers a private route using basic auth added users.json: defines some dummy username/password couples modified index.js: registers new private plugin --- lib/index.js | 4 ++-- lib/private.js | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++ lib/users.json | 10 +++++++++ package.json | 3 ++- 4 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 lib/private.js create mode 100644 lib/users.json diff --git a/lib/index.js b/lib/index.js index 74a77b4..68f665e 100755 --- a/lib/index.js +++ b/lib/index.js @@ -2,7 +2,7 @@ var Hapi = require('hapi'); var Version = require('./version'); - +var Private = require('./private'); // Declare internals @@ -14,7 +14,7 @@ exports.init = function (port, next) { var server = new Hapi.Server(); server.connection({ port: port }); - server.register(Version, function (err) { + server.register([Version, Private], function (err) { if (err) { return next(err); diff --git a/lib/private.js b/lib/private.js new file mode 100644 index 0000000..4ae0733 --- /dev/null +++ b/lib/private.js @@ -0,0 +1,59 @@ +// Load modules + +var Users = require('./users.json'); +var Basic = require('hapi-auth-basic'); + + +// Declare internals + +var internals = { + response: { + template: 'private pageGreetings %username%. Welcome to the private section.', + generate: function (username){ + + return this.template.replace('%username%', username); + } + }, + authStrategyName: 'private-basic', + authValidate: function(username, password, callback) { + + var user = Users[username]; + if (!user) { + return callback(null, false); + } + if (user.password === password){ + return callback(null, true, {username: user.username}); + } + callback(null, false); + } +}; + + +exports.register = function (server, options, next) { + + server.register(Basic, function(err){ + if (err) { + return next(err); + } + server.auth.strategy(internals.authStrategyName, 'basic', { validateFunc: internals.authValidate }); + + server.route({ + method: 'GET', + path: '/private', + config: { + auth: internals.authStrategyName, + description: 'Returns a welcome message if user successfully authenticated via basic auth', + handler: function (request, reply) { + + return reply(internals.response.generate(request.auth.credentials.username)); + } + } + }); + }); + + return next(); +}; + +exports.register.attributes = { + name: 'private' +}; diff --git a/lib/users.json b/lib/users.json new file mode 100644 index 0000000..347c0a0 --- /dev/null +++ b/lib/users.json @@ -0,0 +1,10 @@ +{ + "dummy": { + "username": "dummy", + "password": "12345678" + }, + "jdoe": { + "username": "jdoe", + "password": "qwerty" + } +} \ No newline at end of file diff --git a/package.json b/package.json index e45972e..f890e57 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hueniversity", - "version": "0.0.3", + "version": "0.0.4", "description": "Community learning experiment", "main": "lib/index.js", "repository": { @@ -19,6 +19,7 @@ "homepage": "https://github.com/hueniverse/hueniversity", "dependencies": { "hapi": "8.x.x", + "hapi-auth-basic": "2.x.x", "hoek": "2.x.x" }, "scripts": { From b004ca820a4a1af72a4d0c7922e0c34efad7825b Mon Sep 17 00:00:00 2001 From: Damien Lebreuilly Date: Mon, 20 Apr 2015 17:11:59 +0200 Subject: [PATCH 2/7] bring test coverage to 100% --- Makefile | 2 +- package.json | 2 +- test/private.js | 116 ++++++++++++++++++++++++++++++++++++++++++++++++ test/version.js | 4 +- 4 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 test/private.js diff --git a/Makefile b/Makefile index 83aaf0c..ab2cc70 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ test: @node node_modules/lab/bin/lab -a code test-cov: - @node node_modules/lab/bin/lab -a code -t 100 -L + @node node_modules/lab/bin/lab -a code -t 100 -Lv test-cov-html: @node node_modules/lab/bin/lab -a code -r html -o coverage.html diff --git a/package.json b/package.json index f890e57..b66180e 100755 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "hoek": "2.x.x" }, "scripts": { - "test": "node node_modules/lab/bin/lab -a code -t 100 -L", + "test": "node node_modules/lab/bin/lab -a code -t 100 -Lv", "start": "node lib/start.js" }, "devDependencies": { diff --git a/test/private.js b/test/private.js new file mode 100644 index 0000000..6e2a14d --- /dev/null +++ b/test/private.js @@ -0,0 +1,116 @@ +// Load modules + +var Code = require('code'); +var Lab = require('lab'); +var Hueniversity = require('../lib'); +var Users = require('../lib/users.json'); +var Basic = require('hapi-auth-basic'); + +// Declare internals + +var internals = {}; + +// Test shortcuts + +var lab = exports.lab = Lab.script(); +var describe = lab.experiment; +var expect = Code.expect; +var it = lab.test; + + +describe('/private', function () { + + it('returns unauthorized if user doesn\'t exist', function (done) { + + Hueniversity.init(0, function (err, server) { + + expect(err).to.not.exist(); + + var username = 'test'; + + expect(Users[username]).to.be.undefined(); + + var password = '12345678'; + var request = { method: 'GET', url: '/private', headers: { authorization: internals.header(username, password) } }; + + server.inject(request, function (res) { + + expect(res.statusCode).to.equal(401); + + server.stop(done); + }); + }); + }); + + it('returns unauthorized if user exist but password is wrong', function (done) { + + Hueniversity.init(0, function (err, server) { + + expect(err).to.not.exist(); + + var username = 'jdoe'; + + expect(Users[username]).to.deep.equal({username: 'jdoe', password: 'qwerty'}); + + var password = '12345678'; + var request = { method: 'GET', url: '/private', headers: { authorization: internals.header(username, password) } }; + + server.inject(request, function (res) { + + expect(res.statusCode).to.equal(401); + + server.stop(done); + }); + }); + }); + + it('returns greetings if user exists and password is ok', function (done) { + + Hueniversity.init(0, function (err, server) { + + expect(err).to.not.exist(); + + var username = 'jdoe'; + + expect(Users[username]).to.deep.equal({username: 'jdoe', password: 'qwerty'}); + + var password = 'qwerty'; + var request = { method: 'GET', url: '/private', headers: { authorization: internals.header(username, password) } }; + + server.inject(request, function (res) { + + expect(res.statusCode).to.equal(200); + expect(res.result).to.equal('private pageGreetings jdoe. Welcome to the private section.'); + + server.stop(done); + }); + }); + }); + + it('error bubbles up if basic auth plugin registration throws', { parallel: false }, function (done) { + + var orig = Basic.register; + Basic.register = function (server, options, next) { + + Basic.register = orig; + return next(new Error('register basic auth failed')); + }; + + Basic.register.attributes = { + name: 'fake basic auth' + }; + + Hueniversity.init(0, function (err, server) { + + expect(err).to.exist(); + expect(err.message).to.equal('register basic auth failed'); + + done(); + }); + }); +}); + +internals.header = function (username, password) { + + return 'Basic ' + (new Buffer(username + ':' + password, 'utf8')).toString('base64'); +}; diff --git a/test/version.js b/test/version.js index dfd2601..daf8bd6 100755 --- a/test/version.js +++ b/test/version.js @@ -3,7 +3,7 @@ var Code = require('code'); var Lab = require('lab'); var Pkg = require('../package.json'); -var Server = require('../lib'); +var Hueniversity = require('../lib'); // Test shortcuts @@ -18,7 +18,7 @@ describe('/version', function () { it('returns the version from package.json', function (done) { - Server.init(0, function (err, server) { + Hueniversity.init(0, function (err, server) { expect(err).to.not.exist(); From 6f37df26c9720631995d9e33384fb6d3b48a800d Mon Sep 17 00:00:00 2001 From: Damien Lebreuilly Date: Mon, 20 Apr 2015 17:46:24 +0200 Subject: [PATCH 3/7] fix misplaced return next() causing test to fail --- lib/private.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/private.js b/lib/private.js index 4ae0733..dd76608 100644 --- a/lib/private.js +++ b/lib/private.js @@ -49,9 +49,10 @@ exports.register = function (server, options, next) { } } }); + + return next(); }); - return next(); }; exports.register.attributes = { From 18e06356c175c0a2694f3bb7aad0fed75f834d55 Mon Sep 17 00:00:00 2001 From: Damien Lebreuilly Date: Tue, 21 Apr 2015 18:33:07 +0200 Subject: [PATCH 4/7] removes unused describe variable in test/index.js adds return statement for consistency and removes unnecessary blank line in lib/private.js as pointed by @AdriVanHoudt --- lib/private.js | 3 +-- test/index.js | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/private.js b/lib/private.js index dd76608..7ac6cf1 100644 --- a/lib/private.js +++ b/lib/private.js @@ -24,7 +24,7 @@ var internals = { if (user.password === password){ return callback(null, true, {username: user.username}); } - callback(null, false); + return callback(null, false); } }; @@ -52,7 +52,6 @@ exports.register = function (server, options, next) { return next(); }); - }; exports.register.attributes = { diff --git a/test/index.js b/test/index.js index 2d7900f..b7591d5 100755 --- a/test/index.js +++ b/test/index.js @@ -10,7 +10,6 @@ var Version = require('../lib/version'); // Test shortcuts var lab = exports.lab = Lab.script(); -var describe = lab.experiment; var expect = Code.expect; var it = lab.test; From 8f202520754b2d97cc72b9f5dfab402d5b65bb03 Mon Sep 17 00:00:00 2001 From: Damien Lebreuilly Date: Thu, 23 Apr 2015 11:14:08 +0200 Subject: [PATCH 5/7] adds a level of indirection in users object to allow test users injection. updates single user retrieval in lib/private.js to reflect the change and update tests to inject test users before each test and restore users module after each test --- lib/private.js | 11 ++++++----- lib/users.json | 18 ++++++++++-------- test/private.js | 42 +++++++++++++++++++++++++++++++----------- 3 files changed, 47 insertions(+), 24 deletions(-) diff --git a/lib/private.js b/lib/private.js index 7ac6cf1..275e298 100644 --- a/lib/private.js +++ b/lib/private.js @@ -17,14 +17,15 @@ var internals = { authStrategyName: 'private-basic', authValidate: function(username, password, callback) { - var user = Users[username]; + var candidateUser = Users.collection.filter(function(userObject){ + + return userObject.username === username && userObject.password === password; + }); + var user = candidateUser.length ? candidateUser[0] : null; if (!user) { return callback(null, false); } - if (user.password === password){ - return callback(null, true, {username: user.username}); - } - return callback(null, false); + return callback(null, true, {username: user.username}); } }; diff --git a/lib/users.json b/lib/users.json index 347c0a0..a040dfc 100644 --- a/lib/users.json +++ b/lib/users.json @@ -1,10 +1,12 @@ { - "dummy": { - "username": "dummy", - "password": "12345678" - }, - "jdoe": { - "username": "jdoe", - "password": "qwerty" - } + "collection": [ + { + "username": "dummy", + "password": "12345678" + }, + { + "username": "janedoe", + "password": "azerty" + } + ] } \ No newline at end of file diff --git a/test/private.js b/test/private.js index 6e2a14d..d246bbd 100644 --- a/test/private.js +++ b/test/private.js @@ -5,6 +5,7 @@ var Lab = require('lab'); var Hueniversity = require('../lib'); var Users = require('../lib/users.json'); var Basic = require('hapi-auth-basic'); +var Hoek = require('hoek'); // Declare internals @@ -16,11 +17,25 @@ var lab = exports.lab = Lab.script(); var describe = lab.experiment; var expect = Code.expect; var it = lab.test; +var beforeEach = lab.beforeEach; +var afterEach = lab.afterEach; describe('/private', function () { - it('returns unauthorized if user doesn\'t exist', function (done) { + beforeEach(function(done){ + + internals.injectTestUsersByPatchingUsersModule(); + done(); + }); + + afterEach(function(done){ + + internals.restoreOrigUsersCollectionInUsersModule(); + done(); + }); + + it('returns unauthorized if user doesn\'t exist', { parallel: false }, function (done) { Hueniversity.init(0, function (err, server) { @@ -28,8 +43,6 @@ describe('/private', function () { var username = 'test'; - expect(Users[username]).to.be.undefined(); - var password = '12345678'; var request = { method: 'GET', url: '/private', headers: { authorization: internals.header(username, password) } }; @@ -42,16 +55,13 @@ describe('/private', function () { }); }); - it('returns unauthorized if user exist but password is wrong', function (done) { + it('returns unauthorized if user exist but password is wrong', { parallel: false }, function (done) { Hueniversity.init(0, function (err, server) { expect(err).to.not.exist(); var username = 'jdoe'; - - expect(Users[username]).to.deep.equal({username: 'jdoe', password: 'qwerty'}); - var password = '12345678'; var request = { method: 'GET', url: '/private', headers: { authorization: internals.header(username, password) } }; @@ -64,16 +74,13 @@ describe('/private', function () { }); }); - it('returns greetings if user exists and password is ok', function (done) { + it('returns greetings if user exists and password is ok', { parallel: false }, function (done) { Hueniversity.init(0, function (err, server) { expect(err).to.not.exist(); var username = 'jdoe'; - - expect(Users[username]).to.deep.equal({username: 'jdoe', password: 'qwerty'}); - var password = 'qwerty'; var request = { method: 'GET', url: '/private', headers: { authorization: internals.header(username, password) } }; @@ -114,3 +121,16 @@ internals.header = function (username, password) { return 'Basic ' + (new Buffer(username + ':' + password, 'utf8')).toString('base64'); }; + +internals.testUsers = [{username: 'jdoe', password: 'qwerty'}]; + +internals.injectTestUsersByPatchingUsersModule = function(){ + + internals.origUsersCollection = Users.collection; + Users.collection = Hoek.clone(internals.testUsers); +}; + +internals.restoreOrigUsersCollectionInUsersModule = function(){ + + Users.collection = internals.origUsersCollection; +}; From f202881cc995f1934ca73a7ef317b9d9a9e56f50 Mon Sep 17 00:00:00 2001 From: Damien Lebreuilly Date: Thu, 23 Apr 2015 14:19:31 +0200 Subject: [PATCH 6/7] refactor code so that users module is injected in index.init and taken as parameter by Private module. refactor tests accordingly. --- lib/index.js | 10 +++++++--- lib/private.js | 16 ++++++++++----- lib/start.js | 10 ++++++++-- lib/users.json | 22 ++++++++++----------- test/index.js | 17 +++++++++++++--- test/private.js | 52 ++++++++++++++++++++++++++----------------------- test/version.js | 3 ++- 7 files changed, 80 insertions(+), 50 deletions(-) diff --git a/lib/index.js b/lib/index.js index 68f665e..b4f2c34 100755 --- a/lib/index.js +++ b/lib/index.js @@ -9,12 +9,16 @@ var Private = require('./private'); var internals = {}; -exports.init = function (port, next) { +exports.init = function (config, next) { + + if (!config || typeof (config) !== 'object'){ + return next(new Error('mandatory configuration object not provided')); + } var server = new Hapi.Server(); - server.connection({ port: port }); + server.connection({ port: config.port }); - server.register([Version, Private], function (err) { + server.register([Version, {register: Private, options: {users: config.users}}], function (err) { if (err) { return next(err); diff --git a/lib/private.js b/lib/private.js index 275e298..f1e6898 100644 --- a/lib/private.js +++ b/lib/private.js @@ -1,9 +1,7 @@ // Load modules -var Users = require('./users.json'); var Basic = require('hapi-auth-basic'); - // Declare internals var internals = { @@ -15,9 +13,9 @@ var internals = { } }, authStrategyName: 'private-basic', - authValidate: function(username, password, callback) { + authValidate: function(users, username, password, callback) { - var candidateUser = Users.collection.filter(function(userObject){ + var candidateUser = users.filter(function(userObject){ return userObject.username === username && userObject.password === password; }); @@ -32,11 +30,19 @@ var internals = { exports.register = function (server, options, next) { + var users = options.users; + if (users === null || users === undefined){ + return next(new Error('private plugin requires users to be provided as option')); + } + if (!users.filter || !typeof (users.filter) === 'function'){ + return next(new Error('provided users option must have a filter method')); + } + server.register(Basic, function(err){ if (err) { return next(err); } - server.auth.strategy(internals.authStrategyName, 'basic', { validateFunc: internals.authValidate }); + server.auth.strategy(internals.authStrategyName, 'basic', { validateFunc: internals.authValidate.bind(internals, users) }); server.route({ method: 'GET', diff --git a/lib/start.js b/lib/start.js index 15cefb4..e23a41f 100755 --- a/lib/start.js +++ b/lib/start.js @@ -2,14 +2,20 @@ var Hoek = require('hoek'); var Server = require('./index'); +var Users = require('./users.json'); // Declare internals -var internals = {}; +var internals = { + config: { + port: 8000, + users: Users + } +}; -Server.init(8000, function (err, server) { +Server.init(internals.config, function (err, server) { Hoek.assert(!err, err); console.log('Server started at: ' + server.info.uri); diff --git a/lib/users.json b/lib/users.json index a040dfc..9ba69a0 100644 --- a/lib/users.json +++ b/lib/users.json @@ -1,12 +1,10 @@ -{ - "collection": [ - { - "username": "dummy", - "password": "12345678" - }, - { - "username": "janedoe", - "password": "azerty" - } - ] -} \ No newline at end of file +[ + { + "username": "dummy", + "password": "12345678" + }, + { + "username": "janedoe", + "password": "azerty" + } +] diff --git a/test/index.js b/test/index.js index b7591d5..91fcbfa 100755 --- a/test/index.js +++ b/test/index.js @@ -5,6 +5,7 @@ var Code = require('code'); var Lab = require('lab'); var Hueniversity = require('../lib'); var Version = require('../lib/version'); +var Users = require('../lib/users.json'); // Test shortcuts @@ -13,10 +14,20 @@ var lab = exports.lab = Lab.script(); var expect = Code.expect; var it = lab.test; +it('throws if configuration is null', function(done){ + + Hueniversity.init(null, function (err, server) { + + expect(err).to.exist(); + expect(err.message).to.equal('mandatory configuration object not provided'); + + done(); + }); +}); it('starts server and returns hapi server object', function (done) { - Hueniversity.init(0, function (err, server) { + Hueniversity.init({port: 0, users: Users}, function (err, server) { expect(err).to.not.exist(); expect(server).to.be.instanceof(Hapi.Server); @@ -27,7 +38,7 @@ it('starts server and returns hapi server object', function (done) { it('starts server on provided port', function (done) { - Hueniversity.init(5000, function (err, server) { + Hueniversity.init({port: 5000, users: Users}, function (err, server) { expect(err).to.not.exist(); expect(server.info.port).to.equal(5000); @@ -49,7 +60,7 @@ it('handles register plugin errors', { parallel: false }, function (done) { name: 'fake version' }; - Hueniversity.init(0, function (err, server) { + Hueniversity.init({port: 0, users: Users}, function (err, server) { expect(err).to.exist(); expect(err.message).to.equal('register version failed'); diff --git a/test/private.js b/test/private.js index d246bbd..beef55b 100644 --- a/test/private.js +++ b/test/private.js @@ -3,7 +3,6 @@ var Code = require('code'); var Lab = require('lab'); var Hueniversity = require('../lib'); -var Users = require('../lib/users.json'); var Basic = require('hapi-auth-basic'); var Hoek = require('hoek'); @@ -23,21 +22,10 @@ var afterEach = lab.afterEach; describe('/private', function () { - beforeEach(function(done){ - - internals.injectTestUsersByPatchingUsersModule(); - done(); - }); - - afterEach(function(done){ - - internals.restoreOrigUsersCollectionInUsersModule(); - done(); - }); it('returns unauthorized if user doesn\'t exist', { parallel: false }, function (done) { - Hueniversity.init(0, function (err, server) { + Hueniversity.init({port: 0, users: internals.getTestUsers()}, function (err, server) { expect(err).to.not.exist(); @@ -57,7 +45,7 @@ describe('/private', function () { it('returns unauthorized if user exist but password is wrong', { parallel: false }, function (done) { - Hueniversity.init(0, function (err, server) { + Hueniversity.init({port: 0, users: internals.getTestUsers()}, function (err, server) { expect(err).to.not.exist(); @@ -76,7 +64,7 @@ describe('/private', function () { it('returns greetings if user exists and password is ok', { parallel: false }, function (done) { - Hueniversity.init(0, function (err, server) { + Hueniversity.init({port: 0, users: internals.getTestUsers()}, function (err, server) { expect(err).to.not.exist(); @@ -107,7 +95,7 @@ describe('/private', function () { name: 'fake basic auth' }; - Hueniversity.init(0, function (err, server) { + Hueniversity.init({port: 0, users: internals.getTestUsers()}, function (err, server) { expect(err).to.exist(); expect(err.message).to.equal('register basic auth failed'); @@ -115,6 +103,28 @@ describe('/private', function () { done(); }); }); + + it('throws an error if users aren\'t provided', function(done){ + + Hueniversity.init({port: 0, users: null}, function (err, server) { + + expect(err).to.exist(); + expect(err.message).to.equal('private plugin requires users to be provided as option'); + + done(); + }); + }); + + it('throws an error if provided users object doesn\'t have filter method', function(done){ + + Hueniversity.init({port: 0, users: {}}, function (err, server) { + + expect(err).to.exist(); + expect(err.message).to.equal('provided users option must have a filter method'); + + done(); + }); + }); }); internals.header = function (username, password) { @@ -124,13 +134,7 @@ internals.header = function (username, password) { internals.testUsers = [{username: 'jdoe', password: 'qwerty'}]; -internals.injectTestUsersByPatchingUsersModule = function(){ - - internals.origUsersCollection = Users.collection; - Users.collection = Hoek.clone(internals.testUsers); -}; - -internals.restoreOrigUsersCollectionInUsersModule = function(){ +internals.getTestUsers = function(){ - Users.collection = internals.origUsersCollection; + return Hoek.clone(internals.testUsers); }; diff --git a/test/version.js b/test/version.js index daf8bd6..4406897 100755 --- a/test/version.js +++ b/test/version.js @@ -4,6 +4,7 @@ var Code = require('code'); var Lab = require('lab'); var Pkg = require('../package.json'); var Hueniversity = require('../lib'); +var Users = require('../lib/users.json'); // Test shortcuts @@ -18,7 +19,7 @@ describe('/version', function () { it('returns the version from package.json', function (done) { - Hueniversity.init(0, function (err, server) { + Hueniversity.init({port: 0, users: Users}, function (err, server) { expect(err).to.not.exist(); From 95186f2cd0c0f5f8675783d355bc63453e25bf8d Mon Sep 17 00:00:00 2001 From: Damien Lebreuilly Date: Thu, 23 Apr 2015 16:17:41 +0200 Subject: [PATCH 7/7] removes unnecessary parallel:false statements where monkey patching has been avoided --- test/private.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/private.js b/test/private.js index beef55b..1991c32 100644 --- a/test/private.js +++ b/test/private.js @@ -23,7 +23,7 @@ var afterEach = lab.afterEach; describe('/private', function () { - it('returns unauthorized if user doesn\'t exist', { parallel: false }, function (done) { + it('returns unauthorized if user doesn\'t exist', function (done) { Hueniversity.init({port: 0, users: internals.getTestUsers()}, function (err, server) { @@ -43,7 +43,7 @@ describe('/private', function () { }); }); - it('returns unauthorized if user exist but password is wrong', { parallel: false }, function (done) { + it('returns unauthorized if user exist but password is wrong', function (done) { Hueniversity.init({port: 0, users: internals.getTestUsers()}, function (err, server) { @@ -62,7 +62,7 @@ describe('/private', function () { }); }); - it('returns greetings if user exists and password is ok', { parallel: false }, function (done) { + it('returns greetings if user exists and password is ok', function (done) { Hueniversity.init({port: 0, users: internals.getTestUsers()}, function (err, server) {